captaincore-manager/app/Site.php
2026-04-20 21:45:36 -04:00

1713 lines
No EOL
72 KiB
PHP

<?php
namespace CaptainCore;
class Site {
protected $site_id = "";
protected $environment = "";
public function __construct( $site_id = "", $environment = "production" ) {
$this->site_id = $site_id;
$this->environment = $environment;
}
public function get() {
if ( is_object( $this->site_id ) ) {
$site = $this->site_id;
}
if ( ! isset( $site ) ) {
$site = ( new Sites )->get( $this->site_id );
}
$upload_dir = wp_upload_dir();
// Fetch relating environments
$environments = self::environments();
$upload_uri = get_option( 'options_remote_upload_uri' );
$details = json_decode ( $site->details );
$domain = $site->name;
$customer = $site->account_id;
$mailgun = $details->mailgun;
$production_details = $environments[0]->details;
$visits = $environments[0]->visits;
$subsite_count = $environments[0]->subsite_count;
$production_address = $environments[0]->address;
$production_username = $environments[0]->username;
$production_port = $environments[0]->port;
$database_username = $environments[0]->database_username;
$staging_address = ( isset( $environments[1] ) ? $environments[1]->address : '' );
$staging_username = ( isset( $environments[1] ) ? $environments[1]->username : '' );
$staging_port = ( isset( $environments[1] ) ? $environments[1]->port : '' );
$home_url = $environments[0]->home_url;
// Prepare site details to be returned
$site_details = (object) [];
$site_details->site_id = $site->site_id;
$site_details->account_id = $site->account_id;
$site_details->account = self::account();
$site_details->created_at = $site->created_at;
$site_details->updated_at = $site->updated_at;
$site_details->name = $site->name;
$site_details->key = $details->key;
$site_details->environment_vars = empty( $details->environment_vars ) ? "" : $details->environment_vars;
$site_details->site = $site->site;
$site_details->provider = $site->provider;
$site_details->usage_breakdown = [];
$site_details->timeline = [];
$site_details->loading_plugins = false;
$site_details->loading_themes = false;
$site_details->environment_selected = 'Production';
$site_details->mailgun = $mailgun;
$site_details->status = $site->status;
$site_details->subsite_count = empty( $subsite_count ) ? 0 : $subsite_count;
$site_details->tabs = 'tab-Site-Management';
$site_details->tabs_management = 'tab-Info';
$site_details->core = empty( $environments[0]->core ) ? "" : $environments[0]->core;
$site_details->storage = $details->storage;
$site_details->outdated = false;
if ( ! empty( $visits ) && is_string( $visits ) ) {
$site_details->visits = intval( $visits );
}
$site_details->update_logs = [];
$site_details->update_logs_pagination = [
'descending' => true,
'sortBy' => 'date',
];
$site_details->pagination = [ 'sortBy' => 'roles' ];
// Mark site as outdated if sync older then 48 hours
if ( ! empty( $environments[0]->updated_at ) && strtotime( $environments[0]->updated_at ) <= strtotime( "-48 hours" ) ) {
$site_details->outdated = true;
}
$site_details->errors = [];
if ( ! empty( $production_details->console_errors ) ) {
$site_details->errors = $production_details->console_errors;
}
if ( ! isset( $site_details->visits ) ) {
$site_details->visits = '';
}
if ( $site_details->visits == 0 ) {
$site_details->visits = '';
}
$site_details->users = [];
$site_details->update_logs = [];
$site_details->environments = $environments;
$site_details->screenshot = false;
$site_details->screenshots = [];
if ( $site->screenshot == true ) {
$screenshot_base = $details->screenshot_base;
$screenshot_url_base = "{$upload_uri}/{$site->site}_{$site->site_id}/production/screenshots/{$screenshot_base}";
$site_details->screenshot = true;
$site_details->screenshots = [
'small' => "{$screenshot_url_base}_thumb-100.jpg",
'large' => "{$screenshot_url_base}_thumb-800.jpg"
];
}
return $site_details;
}
public function get_raw() {
// Fetch site from database
$site = ( new Sites )->get( $this->site_id );
// Fetch relating environments from database
$site->environments = ( new Environments )->where( [ "site_id" => $this->site_id ] );
$site->shared_with = ( new AccountSite )->where( [ "site_id" => $this->site_id ] );
return $site;
}
public function create( $site ) {
// Work with array as PHP object
$site = (object) $site;
foreach( $site->environments as $key => $environment ) {
$site->environments[ $key ] = (object) $environment;
}
// Prep for response to return
$response = [ "errors" => [] ];
// Pull in current user
$current_user = wp_get_current_user();
// Validate
if ( $site->name == '' ) {
$response['errors'][] = "Error: Domain can't be empty.";
}
if ( $site->site == '' ) {
$response['errors'][] = "Error: Site can't be empty.";
}
if ( ! ctype_alnum ( $site->site ) ) {
$response['errors'][] = "Error: Site does not consist of all letters or digits.";
}
if ( strlen($site->site) < 3 ) {
$response['errors'][] = "Error: Site length less then 3 characters.";
}
if ( $site->environments[0]->address == "" ) {
$response['errors'][] = "Error: Production environment address can't be empty.";
}
if ( $site->environments[0]->username == "" ) {
$response['errors'][] = "Error: Production environment username can't be empty.";
}
if ( $site->environments[0]->protocol == "" ) {
$response['errors'][] = "Error: Production environment protocol can't be empty.";
}
if ( $site->environments[0]->port == "" ) {
$response['errors'][] = "Error: Production environment port can't be empty.";
}
if ( $site->environments[0]->port != "" and ! ctype_digit( $site->environments[0]->port ) ) {
$response['errors'][] = "Error: Production environment port can only be numbers.";
}
if ( ! empty ( $site->environments[1] ) and $site->environments[1]->port and ! ctype_digit( $site->environments[1]->port ) ) {
$response['errors'][] = "Error: Staging environment port can only be numbers.";
}
// Hunt for conflicting site names
$site_check = Sites::where( [ "site" => $site->site ] );
if ( count( $site_check ) > 0 ) {
$response['errors'][] = "Error: Site name needs to be unique.";
}
if ( count($response['errors']) > 0 ) {
return $response;
}
// Remove staging if empty
if ( empty( $site->environments[1]->address ) ) {
unset( $site->environments[1] );
}
if ( count($response['errors']) > 0 ) {
return $response;
}
$time_now = date("Y-m-d H:i:s");
$details = (object) [
"key" => $site->key,
"environment_vars" => $site->environment_vars,
"subsites" => "",
"storage" => "",
"visits" => "",
"mailgun" => "",
"core" => "",
"verify" => $site->verify,
"remote_key" => empty( $site->remote_key ) ? "" : $site->remote_key,
"backup_settings" => (object) [
"mode" => "direct",
"interval" => "daily",
"active" => true
],
];
$new_site = [
'account_id' => $site->account_id,
'customer_id' => empty( $site->customer_id ) ? "" : $site->customer_id,
'name' => $site->name,
'site' => $site->site,
'provider' => $site->provider,
'provider_id' => empty( $site->provider_id ) ? null : $site->provider_id,
'provider_site_id' => empty( $site->provider_site_id ) ? null : $site->provider_site_id,
'created_at' => $time_now,
'updated_at' => $time_now,
'details' => json_encode( $details ),
'screenshot' => '0',
'status' => 'active',
];
$site_id = Sites::insert( $new_site );
if ( ! is_int( $site_id ) || $site_id == 0 ) {
$response['response'] = json_encode( $new_site );
$response['errors'][] = 'Failed to add new site';
return $response;
}
$response['response'] = 'Successfully added new site';
$response['site_id'] = $site_id;
$this->site_id = $site_id;
$shared_with_ids = [];
foreach( $site->shared_with as $account_id ) {
if ( $site->customer_id == $account_id or $site->account_id == $account_id ) {
continue;
}
$shared_with_ids[] = $account_id;
}
self::assign_accounts( $shared_with_ids );
// Update environments
foreach ( $site->environments as $environment ) {
$new_environment = [
'site_id' => $site_id,
'created_at' => $time_now,
'updated_at' => $time_now,
'environment' => $environment->environment,
'address' => $environment->address,
'username' => $environment->username,
'password' => $environment->password,
'protocol' => $environment->protocol,
'port' => $environment->port,
'home_directory' => $environment->home_directory,
'database_username' => $environment->database_username,
'database_password' => $environment->database_password,
'offload_enabled' => $environment->offload_enabled ?? '',
'offload_access_key' => $environment->offload_access_key ?? '',
'offload_secret_key' => $environment->offload_secret_key ?? '',
'offload_bucket' => $environment->offload_bucket ?? '',
'offload_path' => $environment->offload_path ?? '',
'monitor_enabled' => $environment->monitor_enabled ?? '',
'updates_enabled' => $environment->updates_enabled ?? '',
'updates_exclude_plugins' => $environment->updates_exclude_plugins ?? '',
'updates_exclude_themes' => $environment->updates_exclude_themes ?? '',
];
( new Environments )->insert( $new_environment );
}
// Generate new customer if needed
if ( empty( $site->customer_id ) ) {
$hosting_plans = json_decode( get_option('captaincore_hosting_plans') );
if ( is_array( $hosting_plans ) ) {
$plan = $hosting_plans[0];
$plan->usage = (object) [ "storage" => "0", "visits" => "", "sites" => "" ];
}
$new_account = [
"name" => $site->name,
'created_at' => $time_now,
'updated_at' => $time_now,
'defaults' => json_encode( [ "email" => "", "timezone" => "", "recipes" => [], "users" => [] ] ),
'plan' => json_encode( $plan ),
'metrics' => json_encode( [ "sites" => "1", "users" => "0", "domains" => "0" ] ),
'status' => 'active',
];
$site->customer_id = ( new Accounts )->insert( $new_account );
( new Sites )->update( [ "customer_id" => $site->customer_id ], [ "site_id" => $site_id ] );
}
ActivityLog::log( 'created', 'site', $site_id, $site->name, "Created site {$site->name}", [], $site->customer_id ?? null );
( new Account( $site->account_id, true ) )->calculate_totals();
return $response;
}
public function update( $site ) {
// Work with array as PHP object
$site = (object) $site;
// Prep for response to return
$response = [];
// Validate site exists
$current_site = ( new Sites )->get( $this->site_id );
if ( $current_site == "" ) {
$response['response'] = 'Error: Site ID not found.';
return $response;
}
$account_id_previous = $current_site->account_id;
$time_now = date("Y-m-d H:i:s");
$details = json_decode( $current_site->details );
$details->key = $site->key;
$details->environment_vars = $site->environment_vars;
// Updates post
$update_site = [
'site_id' => $this->site_id,
'account_id' => $site->account_id,
'customer_id' => $site->customer_id,
'name' => $site->name,
'site' => $site->site,
'provider' => $site->provider,
'updated_at' => $time_now,
'details' => json_encode( $details ),
];
$update_response = ( new Sites )->update( $update_site, [ "site_id" => $this->site_id ] );
if ( ! is_int( $update_response ) ) {
$response['response'] = 'Failed updating site';
return $response;
}
ActivityLog::log( 'updated', 'site', $this->site_id, $site->name, "Updated site {$site->name}", [], $site->customer_id ?? null );
$response['response'] = 'Successfully updated site';
$response['site_id'] = $this->site_id;
$environment_ids = self::environment_ids();
$shared_with_ids = [];
foreach( $site->shared_with as $account_id ) {
if ( $site->customer_id == $account_id or $site->account_id == $account_id ) {
continue;
}
$shared_with_ids[] = $account_id;
}
self::assign_accounts( $shared_with_ids );
$new_environment_ids = array_column( $site->environments, "environment_id" );
foreach( $environment_ids as $environment_id ) {
if ( ! in_array( $environment_id, $new_environment_ids ) ) {
( new Environments )->delete( $environment_id );
}
}
// Update environments
$db_environments = new Environments();
foreach ( $site->environments as $environment ) {
// Add as new environment
if ( empty( $environment['environment_id'] ) ) {
$new_environment = [
'site_id' => $this->site_id,
'environment' => "Staging",
'address' => $environment['address'],
'username' => $environment['username'],
'password' => $environment['password'],
'protocol' => $environment['protocol'],
'port' => $environment['port'],
'home_directory' => $environment['home_directory'],
'database_username' => $environment['database_username'],
'database_password' => $environment['database_password'],
'offload_enabled' => $environment['offload_enabled'],
'offload_access_key' => $environment['offload_access_key'],
'offload_secret_key' => $environment['offload_secret_key'],
'offload_bucket' => $environment['offload_bucket'],
'offload_path' => $environment['offload_path'],
'monitor_enabled' => $environment['monitor_enabled'],
'updates_enabled' => $environment['updates_enabled'],
'updates_exclude_plugins' => $environment['updates_exclude_plugins'],
'updates_exclude_themes' => $environment['updates_exclude_themes'],
];
$environment_id = ( new Environments )->insert( $new_environment );
continue;
}
// Verify this environment ID belongs to this site.
if ( ! in_array( $environment['environment_id'], $environment_ids )) {
continue;
}
$update_environment = [
'address' => $environment['address'],
'username' => $environment['username'],
'password' => $environment['password'],
'protocol' => $environment['protocol'],
'port' => $environment['port'],
'home_directory' => $environment['home_directory'],
'database_username' => $environment['database_username'],
'database_password' => $environment['database_password'],
'offload_enabled' => $environment['offload_enabled'],
'offload_access_key' => $environment['offload_access_key'],
'offload_secret_key' => $environment['offload_secret_key'],
'offload_bucket' => $environment['offload_bucket'],
'offload_path' => $environment['offload_path'],
'monitor_enabled' => $environment['monitor_enabled'],
'updates_enabled' => $environment['updates_enabled'],
'updates_exclude_plugins' => $environment['updates_exclude_plugins'],
'updates_exclude_themes' => $environment['updates_exclude_themes'],
];
$db_environments->update( $update_environment, [ 'environment_id' => $environment['environment_id'] ] );
}
( new Account( $account_id_previous, true ) )->calculate_totals();
( new Account( $site->account_id, true ) )->calculate_totals();
return $response;
}
public function update_mailgun( $domain ) {
$site = ( new Sites )->get( $this->site_id );
if ( $site == "" ) {
$response['response'] = 'Error: Site ID not found.';
return $response;
}
$details = json_decode( $site->details );
$details->mailgun = $domain;
( new Sites )->update( [ "details" => json_encode( $details ) ], [ "site_id" => $site->site_id ] );
self::sync();
}
public function sync() {
$command = "site sync {$this->site_id}";
// Disable https when debug enabled
if ( defined( 'CAPTAINCORE_DEBUG' ) ) {
add_filter( 'https_ssl_verify', '__return_false' );
}
$data = [
'timeout' => 45,
'headers' => [
'Content-Type' => 'application/json; charset=utf-8',
'token' => captaincore_get_cli_token()
],
'body' => json_encode( [ "command" => $command ]),
'method' => 'POST',
'data_format' => 'body'
];
// Add command to dispatch server
$response = wp_remote_post( CAPTAINCORE_CLI_ADDRESS . "/run", $data );
if ( is_wp_error( $response ) ) {
$error_message = $response->get_error_message();
return "Something went wrong: $error_message";
}
return $response["body"];
}
/**
* Pull live SFTP/SSH connection details from the site's hosting provider
* and reconcile them with the locally stored environment rows.
*
* Dispatches to the provider class named after $site->provider (e.g.
* CaptainCore\Providers\Kinsta). A provider is considered to support
* remote sync when its class exposes a static fetch_remote_environments()
* method; sites whose provider doesn't yet implement it return a
* "skipped" status rather than an error so fleet-wide sweeps can
* progress past them cleanly.
*
* Returns:
* [
* 'status' => 'in-sync' | 'updated' | 'skipped' | 'error',
* 'message' => string,
* 'changes' => [ [ 'environment', 'field', 'before', 'after' ], ... ],
* 'warnings' => string[],
* ]
*
* @param bool $dry_run When true, diffs are computed but no writes occur.
*/
public function remote_sync( $dry_run = false ) {
$result = [
'status' => 'in-sync',
'message' => '',
'changes' => [],
'warnings' => [],
];
$site = ( new Sites )->get( $this->site_id );
if ( empty( $site ) || empty( $site->site_id ) ) {
$result['status'] = 'error';
$result['message'] = "Site #{$this->site_id} not found";
return $result;
}
// Resolve the provider class and verify it implements the fetch convention.
if ( empty( $site->provider ) ) {
$result['status'] = 'skipped';
$result['message'] = 'Site has no provider configured';
return $result;
}
$provider_class = 'CaptainCore\\Providers\\' . ucfirst( $site->provider );
if ( ! class_exists( $provider_class ) || ! method_exists( $provider_class, 'fetch_remote_environments' ) ) {
$result['status'] = 'skipped';
$result['message'] = "Provider '{$site->provider}' does not implement remote sync yet";
return $result;
}
// Skip multi-tenant "stacked" sites. They share a Kinsta environment
// with a host site and are distinguished by custom environment vars
// (e.g. STACKED_SITE_ID); the provider API can't see them, so pulling
// credentials would overwrite the tenant's values with the host's.
$details = $this->decode_details( $site );
$env_vars = $details['environment_vars'] ?? null;
if ( is_array( $env_vars ) && ! empty( $env_vars ) ) {
$result['status'] = 'skipped';
$result['message'] = 'Site has custom environment vars (multi-tenant setup)';
if ( ! $dry_run ) {
$this->record_remote_sync_timestamp( $site, $result['status'] );
}
return $result;
}
// Resolve provider_site_id, auto-discovering if missing.
$provider_site_id = $site->provider_site_id;
if ( empty( $provider_site_id ) ) {
$resolved = $this->resolve_provider_site_id( $site, $provider_class );
if ( is_wp_error( $resolved ) ) {
$result['status'] = 'error';
$result['message'] = $resolved->get_error_message();
$candidates = $resolved->get_error_data( 'candidates' );
if ( ! empty( $candidates ) ) {
$result['warnings'][] = 'Multiple matches: ' . wp_json_encode( $candidates );
}
return $result;
}
$provider_site_id = $resolved;
if ( ! $dry_run ) {
( new Sites )->update(
[ 'provider_site_id' => $provider_site_id, 'updated_at' => date( 'Y-m-d H:i:s' ) ],
[ 'site_id' => $this->site_id ]
);
ActivityLog::log( 'updated', 'site', $this->site_id, $site->name, "Auto-resolved {$site->provider} provider_site_id to {$provider_site_id}" );
}
$result['warnings'][] = "Auto-resolved missing provider_site_id to {$provider_site_id}";
$site->provider_site_id = $provider_site_id;
}
// Fetch normalized envs from the provider.
$remote_envs = $provider_class::fetch_remote_environments( $provider_site_id, $site->provider_id );
if ( is_wp_error( $remote_envs ) ) {
$result['status'] = 'error';
$result['message'] = $remote_envs->get_error_message();
return $result;
}
$remote_by_label = [];
foreach ( $remote_envs as $remote_env ) {
$remote_by_label[ $remote_env['environment'] ] = $remote_env;
}
$local_envs = ( new Environments )->where( [ 'site_id' => $this->site_id ] );
$fields = [ 'address', 'port', 'username', 'password', 'home_directory' ];
$local_labels = [];
foreach ( $local_envs as $local_env ) {
$local_labels[] = $local_env->environment;
if ( ! isset( $remote_by_label[ $local_env->environment ] ) ) {
$result['warnings'][] = "Local '{$local_env->environment}' environment has no match on {$site->provider}";
continue;
}
$remote_env = $remote_by_label[ $local_env->environment ];
$update = [];
foreach ( $fields as $field ) {
$local_value = (string) ( $local_env->$field ?? '' );
$remote_value = (string) ( $remote_env[ $field ] ?? '' );
if ( $remote_value === '' ) {
// Don't blow away local values with an empty remote response.
continue;
}
if ( $local_value !== $remote_value ) {
$update[ $field ] = $remote_value;
$result['changes'][] = [
'environment' => $local_env->environment,
'field' => $field,
'before' => self::mask_value( $field, $local_value ),
'after' => self::mask_value( $field, $remote_value ),
];
}
}
if ( ! empty( $update ) && ! $dry_run ) {
$update['updated_at'] = date( 'Y-m-d H:i:s' );
( new Environments )->update( $update, [ 'environment_id' => $local_env->environment_id ] );
}
}
// Surface remote envs the local DB doesn't track.
foreach ( $remote_by_label as $label => $remote_env ) {
if ( ! in_array( $label, $local_labels, true ) ) {
$result['warnings'][] = "{$site->provider} has '{$label}' environment but it isn't tracked locally";
}
}
if ( ! empty( $result['changes'] ) ) {
$result['status'] = 'updated';
$result['message'] = count( $result['changes'] ) . " field(s) updated from {$site->provider}";
if ( ! $dry_run ) {
$summary = [];
foreach ( $result['changes'] as $change ) {
$summary[] = "{$change['environment']}.{$change['field']}";
}
ActivityLog::log( 'updated', 'site', $this->site_id, $site->name, 'Remote sync updated: ' . implode( ', ', $summary ) );
// Push corrected credentials down to the CLI worker synchronously
// via /run. This mirrors the API path used by the site dialog's
// "update" action and makes sure the rclone configs are actually
// regenerated before we move on to the next site. Queued /tasks
// dispatch was unreliable in the WP-CLI context.
$sync_response = Run::execute( "site sync {$this->site_id}" );
if ( is_wp_error( $sync_response ) ) {
$result['warnings'][] = 'Site sync dispatch failed: ' . $sync_response->get_error_message();
}
}
} else {
$result['message'] = "All environments already in sync with {$site->provider}";
}
// Stamp the sync attempt in details.last_remote_sync_at so fleet-wide
// sweeps can order by staleness. Write for updated / in-sync outcomes;
// errors are left alone so they're retried on the next pass.
if ( ! $dry_run ) {
$this->record_remote_sync_timestamp( $site, $result['status'] );
}
return $result;
}
/**
* Persist a last_remote_sync_at stamp into wp_captaincore_sites.details.
* Writes for successful and skipped outcomes so fleet-wide --stale
* filters work uniformly. Errors are left alone so they're retried on
* the next pass.
*/
protected function record_remote_sync_timestamp( $site, $status ) {
if ( ! in_array( $status, [ 'in-sync', 'updated', 'skipped' ], true ) ) {
return;
}
$details = $this->decode_details( $site );
$details['last_remote_sync_at'] = gmdate( 'Y-m-d H:i:s' );
( new Sites )->update(
[ 'details' => wp_json_encode( $details ) ],
[ 'site_id' => $this->site_id ]
);
}
/**
* Decode the site's details JSON blob into an associative array.
* Returns an empty array for empty / invalid values.
*/
protected function decode_details( $site ) {
if ( empty( $site->details ) ) {
return [];
}
$decoded = json_decode( $site->details, true );
return is_array( $decoded ) ? $decoded : [];
}
/**
* Find the matching remote site id for a local site when the
* provider_site_id is missing. Uses the provider class's fetch_remote_sites()
* method and matches its slug/name against the local site name and tracked
* domains. Returns the remote id, or a WP_Error with 'candidates' data when
* 0 or >1 sites match.
*/
protected function resolve_provider_site_id( $site, $provider_class ) {
if ( ! method_exists( $provider_class, 'fetch_remote_sites' ) ) {
return new \WP_Error( 'provider_no_site_list', "Provider '{$site->provider}' does not expose a site list for auto-resolution" );
}
$remote_sites = $provider_class::fetch_remote_sites( $site->provider_id );
if ( empty( $remote_sites ) ) {
return new \WP_Error( 'provider_no_remote_sites', "Could not fetch site list from {$site->provider}" );
}
// Build the set of local strings to match against:
// - the site's `name` (often a domain like example.com)
// - the bare hostname label of that name (e.g. austinginder from austinginder.kinsta.cloud)
// - all tracked domains for this site, plus their bare hostnames
$needles = [];
$add_needle = function ( $value ) use ( &$needles ) {
$value = strtolower( trim( (string) $value ) );
if ( $value === '' ) {
return;
}
$needles[ $value ] = true;
// Strip leading "www."
if ( strpos( $value, 'www.' ) === 0 ) {
$needles[ substr( $value, 4 ) ] = true;
}
// First DNS label (e.g. "austinginder" from "austinginder.kinsta.cloud")
$first_label = strtok( $value, '.' );
if ( $first_label && $first_label !== $value ) {
$needles[ $first_label ] = true;
}
};
$add_needle( $site->name );
foreach ( $this->domains() as $domain ) {
$add_needle( $domain->name );
}
$matches = [];
foreach ( $remote_sites as $remote ) {
$candidates = [
strtolower( (string) ( $remote['slug'] ?? '' ) ),
strtolower( (string) ( $remote['name'] ?? '' ) ),
];
foreach ( $candidates as $candidate ) {
if ( $candidate !== '' && isset( $needles[ $candidate ] ) ) {
$matches[ $remote['remote_id'] ] = $remote;
break;
}
}
}
$matches = array_values( $matches );
if ( count( $matches ) === 1 ) {
return $matches[0]['remote_id'];
}
if ( count( $matches ) > 1 ) {
return new \WP_Error(
'provider_multiple_matches',
"Multiple {$site->provider} sites match '{$site->name}'. Set provider_site_id manually.",
[ 'candidates' => $matches ]
);
}
return new \WP_Error(
'provider_no_match',
"No {$site->provider} site matches '{$site->name}'. Set provider_site_id manually."
);
}
/**
* Mask sensitive values for response payloads. Only the password field is
* masked; everything else is returned verbatim so admins can see what
* changed without exposing the actual secret in logs / browser history.
*/
protected static function mask_value( $field, $value ) {
if ( $field !== 'password' ) {
return (string) $value;
}
$value = (string) $value;
if ( $value === '' ) {
return '';
}
if ( strlen( $value ) <= 4 ) {
return '****';
}
return '****' . substr( $value, -4 );
}
public function insert_accounts( $account_ids = [] ) {
$accountsite = new AccountSite();
foreach( $account_ids as $account_id ) {
// Fetch current records
$lookup = $accountsite->where( [ "site_id" => $this->site_id, "account_id" => $account_id ] );
// Add new record
if ( count($lookup) == 0 ) {
$accountsite->insert( [ "site_id" => $this->site_id, "account_id" => $account_id ] );
}
}
}
public function assign_accounts( $account_ids = [] ) {
$site_name = ( new Sites )->get( $this->site_id )->name ?? '';
$accountsite = new AccountSite();
// Fetch current records
$current_account_ids = array_column ( $accountsite->where( [ "site_id" => $this->site_id ] ), "account_id" );
// Removed current records not found new records.
foreach ( array_diff( $current_account_ids, $account_ids ) as $account_id ) {
$records = $accountsite->where( [ "site_id" => $this->site_id, "account_id" => $account_id ] );
foreach ( $records as $record ) {
$accountsite->delete( $record->account_site_id );
}
ActivityLog::log( 'unshared', 'site', $this->site_id, $site_name, "Unshared site {$site_name} with account", [], $account_id );
}
// Add new records
foreach ( array_diff( $account_ids, $current_account_ids ) as $account_id ) {
$accountsite->insert( [ "site_id" => $this->site_id, "account_id" => $account_id ] );
ActivityLog::log( 'shared', 'site', $this->site_id, $site_name, "Shared site {$site_name} with account", [], $account_id );
}
// Calculate new totals
$all_account_ids = array_unique( array_merge ( $account_ids, $current_account_ids ) );
foreach ( $all_account_ids as $account_id ) {
( new Account( $account_id, true ) )->calculate_totals();
}
}
public function mark_inactive() {
$site = self::get();
ActivityLog::log( 'deleted', 'site', $this->site_id, $site->name, "Deleted site {$site->name}", [], $site->customer_id ?? null );
$time_now = date("Y-m-d H:i:s");
( new Sites )->update( [ "status" => "inactive", "updated_at" => $time_now ], [ "site_id" => $this->site_id ] );
( new Account( $site->account_id ) )->calculate_usage();
}
public function delete() {
( new Sites )->delete( $this->site_id );
}
public function captures( $environment = "production" ) {
$environment_id = self::fetch_environment_id( $environment );
$captures = new Captures();
$results = $captures->where( [ "site_id" => $this->site_id, "environment_id" => $environment_id ] );
$upload_uri = get_option( 'options_remote_upload_uri' );
$site = ( new Sites )->get( $this->site_id );
$image_base_url = $upload_uri . $site->site . '_' . $this->site_id . '/' . strtolower( $environment ) . '/captures/';
foreach ( $results as $result ) {
$created_at_friendly = new \DateTime( $result->created_at );
$created_at_friendly->setTimezone( new \DateTimeZone( get_option( 'gmt_offset' ) ) );
$created_at_friendly = date_format( $created_at_friendly, 'D, M jS Y g:i a');
$result->created_at_friendly = $created_at_friendly;
$result->pages = json_decode( $result->pages );
$result->image_base_url = $image_base_url;
foreach ( $result->pages as $page ) {
$page->image_url = $image_base_url . str_replace( '#', '%23', $page->image );
}
}
return $results;
}
public function quicksave_get( $hash, $environment = "production" ) {
$command = "quicksave get {$this->site_id}-$environment $hash";
// Disable https when debug enabled
if ( defined( 'CAPTAINCORE_DEBUG' ) ) {
add_filter( 'https_ssl_verify', '__return_false' );
}
$data = [
'timeout' => 45,
'headers' => [
'Content-Type' => 'application/json; charset=utf-8',
'token' => captaincore_get_cli_token()
],
'body' => json_encode( [ "command" => $command ]),
'method' => 'POST',
'data_format' => 'body'
];
// Add command to dispatch server
$response = wp_remote_post( CAPTAINCORE_CLI_ADDRESS . "/run", $data );
if ( is_wp_error( $response ) ) {
$error_message = $response->get_error_message();
return [];
}
$json = json_decode( $response["body"] );
if ( json_last_error() != JSON_ERROR_NONE ) {
return [];
}
return $json;
}
public function backup_show_file( $backup_id, $file_id, $environment = "production" ) {
$file = base64_encode( $file_id );
$command = "backup show {$this->site_id}-$environment $backup_id $file";
// Disable https when debug enabled
if ( defined( 'CAPTAINCORE_DEBUG' ) ) {
add_filter( 'https_ssl_verify', '__return_false' );
}
return Run::CLI_Stream( $command );
}
public function backup_get( $backup_id, $environment = "production" ) {
$command = "backup get {$this->site_id}-$environment $backup_id";
// Disable https when debug enabled
if ( defined( 'CAPTAINCORE_DEBUG' ) ) {
add_filter( 'https_ssl_verify', '__return_false' );
}
$data = [
'timeout' => 45,
'headers' => [
'Content-Type' => 'application/json; charset=utf-8',
'token' => captaincore_get_cli_token()
],
'body' => json_encode( [ "command" => $command ]),
'method' => 'POST',
'data_format' => 'body'
];
// Add command to dispatch server
$response = wp_remote_post( CAPTAINCORE_CLI_ADDRESS . "/run", $data );
if ( is_wp_error( $response ) ) {
$error_message = $response->get_error_message();
return [];
}
return $response["body"];
}
public function backups( $environment = "production" ) {
$command = "backup list {$this->site_id}-$environment --format=json";
// Disable https when debug enabled
if ( defined( 'CAPTAINCORE_DEBUG' ) ) {
add_filter( 'https_ssl_verify', '__return_false' );
}
$data = [
'timeout' => 45,
'headers' => [
'Content-Type' => 'application/json; charset=utf-8',
'token' => captaincore_get_cli_token()
],
'body' => json_encode( [ "command" => $command ]),
'method' => 'POST',
'data_format' => 'body'
];
// Add command to dispatch server
$response = wp_remote_post( CAPTAINCORE_CLI_ADDRESS . "/run", $data );
if ( is_wp_error( $response ) ) {
$error_message = $response->get_error_message();
return "Something went wrong: $error_message";
}
$result = json_decode( $response["body"] );
if ( json_last_error() != JSON_ERROR_NONE ) {
return [];
}
foreach( $result as $item ) {
$item->loading = true;
$item->omitted = false;
$item->files = [];
$item->tree = [];
$item->active = [];
$item->preview = "";
}
usort( $result, function ($a, $b) { return ( $a->time < $b->time ); });
return $result;
}
public function logs_archive_list( $environment = "production" ) {
$command = "logs archive-list {$this->site_id}-$environment";
if ( defined( 'CAPTAINCORE_DEBUG' ) ) {
add_filter( 'https_ssl_verify', '__return_false' );
}
$data = [
'timeout' => 45,
'headers' => [
'Content-Type' => 'application/json; charset=utf-8',
'token' => captaincore_get_cli_token()
],
'body' => json_encode( [ "command" => $command ]),
'method' => 'POST',
'data_format' => 'body'
];
$response = wp_remote_post( CAPTAINCORE_CLI_ADDRESS . "/run", $data );
if ( is_wp_error( $response ) ) {
return [];
}
$result = json_decode( $response["body"] );
if ( json_last_error() != JSON_ERROR_NONE ) {
return [];
}
return $result;
}
public function logs_archive_get( $file, $environment = "production" ) {
$command = "logs archive-get {$this->site_id}-$environment $file";
if ( defined( 'CAPTAINCORE_DEBUG' ) ) {
add_filter( 'https_ssl_verify', '__return_false' );
}
$data = [
'timeout' => 45,
'headers' => [
'Content-Type' => 'application/json; charset=utf-8',
'token' => captaincore_get_cli_token()
],
'body' => json_encode( [ "command" => $command ]),
'method' => 'POST',
'data_format' => 'body'
];
$response = wp_remote_post( CAPTAINCORE_CLI_ADDRESS . "/run", $data );
if ( is_wp_error( $response ) ) {
return new \WP_Error( 'cli_error', $response->get_error_message(), [ 'status' => 500 ] );
}
$result = json_decode( $response["body"] );
if ( json_last_error() != JSON_ERROR_NONE ) {
return new \WP_Error( 'cli_parse_error', 'Invalid response from CLI', [ 'status' => 500 ] );
}
return $result;
}
public function generate_screenshot() {
$site = Sites::get( $this->site_id );
$environments = self::environments();
foreach( $environments as $environment ) {
$capture = Captures::latest_capture( [ "site_id" => $this->site_id, "environment_id" => $environment->environment_id ] );
if ( empty( $capture ) ) {
continue;
}
$created_at = strtotime( $capture->created_at );
$git_commit_short = substr( $capture->git_commit, 0, 7 );
$details = isset( $environment->details ) ? $environment->details : (object) [];
$details->screenshot_base = "{$created_at}_${git_commit_short}";
Environments::update( [ "screenshot" => true, "details" => json_encode( $details ) ], [ "environment_id" => $environment->environment_id ] );
// Update sites if needed
if ( $environment->environment == "Production" ) {
$details = json_decode( $site->details );
$details->screenshot_base = "{$created_at}_${git_commit_short}";
Sites::update( [ "screenshot" => true, "details" => json_encode( $details ) ], [ "site_id" => $site->site_id ] );
}
}
self::sync();
}
public function customer() {
$customer = (object) [];
if ( $customer ) {
foreach ( $customer as $customer_id ) {
$customer_name = get_post_field( 'post_title', $customer_id, 'raw' );
$addons = get_field( 'addons', $customer_id );
if ( $addons == '' ) {
$addons = [];
}
$site_details->customer = array(
'customer_id' => $customer_id,
'name' => $customer_name,
'hosting_addons' => $addons,
'hosting_plan' => array(
'name' => get_field( 'hosting_plan', $customer_id ),
'visits_limit' => get_field( 'visits_limit', $customer_id ),
'storage_limit' => get_field( 'storage_limit', $customer_id ),
'sites_limit' => get_field( 'sites_limit', $customer_id ),
'price' => get_field( 'price', $customer_id ),
),
'usage' => array(
'storage' => get_field( 'storage', $customer_id ),
'visits' => get_field( 'visits', $customer_id ),
'sites' => get_field( 'sites', $customer_id ),
),
);
}
}
if ( count( $site_details->customer ) == 0 ) {
$site_details->customer = array(
'customer_id' => '',
'name' => '',
'hosting_plan' => '',
'visits_limit' => '',
'storage_limit' => '',
'sites_limit' => '',
);
}
}
public function account() {
$site = ( new Sites )->get( $this->site_id );
$account = ( new Accounts )->get( $site->account_id );
$plan = empty( $account->plan ) ? "" : json_decode( $account->plan );
$results = [
'account_id' => $site->account_id,
'name' => empty( $account->name ) ? "" : $account->name,
'plan' => $plan,
'defaults' => empty( $account->defaults ) ? "" : json_decode( $account->defaults ),
];
return $results;
}
public function domains() {
$site = ( new Sites )->get( $this->site_id );
$domains = [];
$domain_ids = ( new AccountDomain )->where( [ "account_id" => $site->customer_id ] );
$domain_ids = array_column( $domain_ids, "domain_id" );
$domain_ids = array_filter( array_unique( $domain_ids ) );
foreach ( $domain_ids as $domain_id ) {
$domain = ( new Domains )->get( $domain_id );
if ( empty( $domain->name ) || empty( $domain->domain_id ) ) {
continue;
}
$domains[] = (object) [
"domain_id" => $domain->domain_id,
"name" => $domain->name,
];
}
return $domains;
}
public function shared_with() {
$site = ( new Sites )->get( $this->site_id );
$accounts = [];
$account_ids = ( new AccountSite )->where( [ "site_id" => $this->site_id ] );
$account_ids = array_column( $account_ids, "account_id" );
$account_ids[] = $site->account_id;
$account_ids[] = $site->customer_id;
$account_ids = array_filter( array_unique( $account_ids ) );
foreach ( $account_ids as $account_id ) {
$account = ( new Accounts )->get( $account_id );
$accounts[] = (object) [
"account_id" => $account->account_id,
"name" => $account->name,
];
}
return $accounts;
}
public function shared_with_ids() {
return array_column( self::shared_with(), 'account_id' );
}
public function fetch() {
$site = ( new Sites )->get( $this->site_id );
$details = json_decode( $site->details );
$site->filtered = true;
$site->removed = isset( $details->removed ) ? $details->removed : false;
$site->loading = false;
$site->key = empty( $details->key ) ? null : $details->key;
$site->core = $details->core;
$site->mailgun = $details->mailgun;
$site->console_errors = isset( $details->console_errors ) ? $details->console_errors : "";
$site->environment_vars = isset( $details->environment_vars ) ? $details->environment_vars : [];
$site->backup_settings = isset( $details->backup_settings ) ? $details->backup_settings : (object) [ "mode" => "direct", "interval" => "daily", "active" => true ];
$site->subsites = $details->subsites;
$site->storage = $details->storage;
$site->visits = $details->visits;
$site->outdated = false;
$site->screenshot_base = isset( $details->screenshot_base ) ? $details->screenshot_base : "";
// Mark site as outdated if sync older then 48 hours
if ( strtotime( $site->updated_at ) <= strtotime( "-48 hours" ) ) {
$site->outdated = true;
}
unset( $site->token );
unset( $site->created_at );
unset( $site->details );
unset( $site->status );
unset( $site->site_usage );
return $site;
}
public function environment_ids() {
$environment_ids = ( new Environments )->where( [ "site_id" => $this->site_id ] );
return array_column( $environment_ids, "environment_id" );
}
public function fetch_environment_id( $environment ) {
$environment_id = ( new Environments )->where( [ "site_id" => $this->site_id, "environment" => $environment ] );
if ( empty( $environment_id ) ) {
return;
}
return array_column( $environment_id, "environment_id" )[0];
}
public function fetch_phpmyadmin() {
$site = ( new Sites )->get( $this->site_id );
if ( $site->provider == "rocketdotnet" ) {
$api_request = "https://api.rocket.net/v1/sites/{$site->provider_id}/pma/login";
$response = wp_remote_get( $api_request , [
'headers' => [
'Authorization' => 'Bearer ' . \CaptainCore\Providers\Rocketdotnet::credentials("token"),
'accept' => 'application/json',
]
]);
if ( is_wp_error( $response ) ) {
return $response->get_error_message();
}
$response = json_decode( $response['body'] );
if ( ! empty( $response->result ) ) {
return $response->result->phpmyadmin_sign_on_url;
}
}
if ( $site->provider == "kinsta" ) {
return \CaptainCore\Providers\Kinsta::get_phpmyadmin_url( $this->site_id, $this->environment );
}
}
public function environments() {
// Fetch relating environments
$site = Sites::get( $this->site_id );
$environments = Environments::fetch_environments( $this->site_id );
$upload_uri = get_option( 'options_remote_upload_uri' );
foreach ( $environments as $environment ) {
$environment_name = strtolower( $environment->environment );
$details = ( isset( $environment->details ) ? json_decode( $environment->details ) : (object) [] );
$environment->captures = count ( self::captures( $environment_name ) );
$environment->screenshots = [];
// Extract screenshot_base from the ENVIRONMENT details, not the site details
$screenshot_base = empty( $details->screenshot_base ) ? "" : $details->screenshot_base;
// Pass this back to the frontend so Vue knows a screenshot exists
$environment->screenshot_base = $screenshot_base;
// If we have a base, construct the full URLs
if ( ! empty( $screenshot_base ) ) {
$screenshot_url_base = "{$upload_uri}/{$site->site}_{$site->site_id}/$environment_name/screenshots/{$screenshot_base}";
$environment->screenshots = [
'small' => "{$screenshot_url_base}_thumb-100.jpg",
'large' => "{$screenshot_url_base}_thumb-800.jpg"
];
}
$environment->fathom_analytics = ( ! empty( $details->fathom ) ? $details->fathom : [] );
if ( $site->provider == 'kinsta' || $site->provider == 'rocketdotnet' ) {
$environment->ssh = "ssh {$environment->username}@{$environment->address} -p {$environment->port}";
}
if ( $site->provider == 'kinsta' and $environment->database_username ) {
$address_array = explode( ".", $environment->address );
$kinsta_ending = array_pop( $address_array );
if ( $kinsta_ending != "com" && $kinsta_ending != "cloud" ) {
$kinsta_ending = "cloud";
}
$environment->database = "https://mysqleditor-{$environment->database_username}.kinsta.{$kinsta_ending}";
if ( $environment->environment == "Staging" ) {
$environment->database = "https://mysqleditor-staging-{$environment->database_username}.kinsta.{$kinsta_ending}";
}
if ( preg_match('/.+\.temp.+?\.kinsta.cloud/', $environment->address, $matches ) ) {
$environment->database = "https://mysqleditor-{$environment->address}";
}
if ( preg_match('/.+\.temp.+?\.kinsta.cloud/', $environment->address, $matches ) && $environment->environment == "Staging" ) {
$environment->database = "https://mysqleditor-staging-{$environment->address}";
}
}
$environment->environment_id = $environment->environment_id;
$environment->details = json_decode( $environment->details );
$environment->link = $environment->home_url;
$environment->fathom = json_decode( $environment->fathom );
$environment->plugins = json_decode( $environment->plugins );
$environment->themes = json_decode( $environment->themes );
$environment->stats = 'Loading';
$environment->stats_password = '';
$environment->users = 'Loading';
$environment->users_search = '';
$environment->backups = 'Loading';
$environment->quicksaves = 'Loading';
$environment->snapshots = 'Loading';
$environment->update_logs = 'Loading';
$environment->server_logs = [ "files" => [] ];
$environment->view_server_logs = false;
$environment->loading_server_logs = false;
$environment->server_log_limit = "1000";
$environment->server_log_selected = "";
$environment->server_log_response = "";
$environment->server_log_lines = [];
$environment->server_log_search = "";
$environment->quicksave_panel = [];
$environment->quicksave_search = '';
$environment->capture_pages = json_decode ( $environment->capture_pages );
$environment->monitor_enabled = intval( $environment->monitor_enabled );
$environment->updates_enabled = intval( $environment->updates_enabled );
$environment->updates_exclude_plugins = !empty($environment->updates_exclude_plugins) ? explode(",", $environment->updates_exclude_plugins) : [];
$environment->updates_exclude_themes = !empty($environment->updates_exclude_themes) ? explode(",", $environment->updates_exclude_themes) : [];
$environment->themes_selected = [];
$environment->plugins_selected = [];
$environment->users_selected = [];
$environment->expanded_backups = [];
if ( $environment->details == "" ) {
$environment->details = [];
}
if ( $environment->themes == "" ) {
$environment->themes = [];
}
if ( $environment->plugins == "" ) {
$environment->plugins = [];
}
if ( $environment->fathom == "" ) {
$environment->fathom = [ [ "domain" => "", "code" => ""] ];
}
$scheduled_scripts = \CaptainCore\Scripts::where( [ "environment_id" => $environment->environment_id, "status" => "scheduled" ] );
foreach ( $scheduled_scripts as $scheduled_script ) {
$details = json_decode( $scheduled_script->details );
$scheduled_script->author = get_the_author_meta( 'display_name', $scheduled_script->user_id );
$scheduled_script->author_avatar = "https://www.gravatar.com/avatar/" . md5( get_the_author_meta( 'email', $scheduled_script->user_id ) ) . "?s=80&d=mp";
$scheduled_script->run_at = $details->run_at;
unset( $scheduled_script->user_id );
}
$environment->scheduled_scripts = $scheduled_scripts;
}
return $environments;
}
public function environments_bare() {
// Fetch relating environments
$db_environments = new Environments();
$environments = $db_environments->fetch_environments( $this->site_id );
$results = [];
foreach ($environments as $environment) {
$result = [
"themes" => json_decode( $environment->themes ),
"plugins" => json_decode( $environment->plugins ),
"details" => json_decode( $environment->details ),
];
if ( $result["themes"] == "" ) {
$result["themes"] = [];
}
if ( $result["plugins"] == "" ) {
$result["plugins"] = [];
}
$results[] = $result;
}
if ( count( $results ) == 0 ) {
return [[ "themes" => [], "plugins" => [] ]];
}
return $results;
}
public function stats_sharing( $fathom_id = "", $sharing = "", $share_password = "" ) {
$environments = self::environments();
$fathom_ids = [];
foreach( $environments as $environment ) {
$environment_fathom_ids = array_column( $environment->fathom_analytics, "code" );
foreach ( $environment_fathom_ids as $id ) {
$fathom_ids[] = strtolower( $id );
}
}
if ( ! in_array( strtolower( $fathom_id ), $fathom_ids ) ) {
return;
}
$url = "https://api.usefathom.com/v1/sites/$fathom_id";
$response = wp_remote_post( $url, [
"headers" => [ "Authorization" => "Bearer " . \CaptainCore\Providers\Fathom::credentials("api_key") ],
'body' => [ "sharing" => $sharing, "share_password" => $share_password ],
] );
return json_decode( $response['body'] );
}
public function stats( $environment = "production", $before = "", $after = "", $grouping = "month", $fathom_id = "" ) {
if ( empty( $after ) ) {
$after = date( 'Y-m-d H:i:s' );
}
if ( empty( $before ) ) {
$date = strtotime("$after -1 year" );
$before = date('Y-m-d H:i:s', $date);
}
if ( is_numeric( $before ) ) {
$before = date( 'Y-m-d H:i:s', $before );
}
if ( is_numeric( $after ) ) {
$after = date( 'Y-m-d H:i:s', $after );
}
$environments = self::environments();
foreach( $environments as $e ) {
if ( strtolower( $e->environment ) == strtolower( $environment ) ) {
$selected_environment = $e;
}
}
if ( empty( $selected_environment ) ) {
return;
}
$fathom_ids = array_column( $selected_environment->fathom_analytics, "code" );
if ( empty( $fathom_ids ) ) {
return [ "Error" => "There was a problem retrieving stats." ];
}
if ( empty( $fathom_id ) ) {
$fathom_id = $fathom_ids[0];
}
$url = "https://api.usefathom.com/v1/aggregations?entity=pageview&entity_id=$fathom_id&aggregates=visits,pageviews,avg_duration,bounce_rate&date_from=$before&date_to=$after&date_grouping=$grouping&sort_by=timestamp:asc";
$response = wp_remote_get( $url, [
"headers" => [ "Authorization" => "Bearer " . \CaptainCore\Providers\Fathom::credentials("api_key") ],
] );
$stats = json_decode( $response['body'] );
if ( $grouping == "hour" ) {
foreach ( $stats as $stat ) {
$stat->date = date('M d Y ga', strtotime( $stat->date ) );
}
}
if ( $grouping == "day" ) {
foreach ( $stats as $stat ) {
$stat->date = date('M d Y', strtotime( $stat->date ) );
}
}
if ( $grouping == "month" ) {
foreach ( $stats as $stat ) {
$stat->date = date('M Y', strtotime( $stat->date ) );
}
}
if ( $grouping == "year" ) {
foreach ( $stats as $stat ) {
$stat->date = date('Y', strtotime( $stat->date ) );
}
}
$url = "https://api.usefathom.com/v1/sites/$fathom_id";
$response = wp_remote_get( $url, [
"headers" => [ "Authorization" => "Bearer " . \CaptainCore\Providers\Fathom::credentials("api_key") ],
] );
$site = json_decode( $response['body'] );
$stats_count = count( $stats );
$response = [
"fathom_id" => $fathom_id,
"site" => $site,
"summary" => [
"pageviews" => array_sum( array_column( $stats, "pageviews" ) ),
"visits" => array_sum( array_column( $stats, "visits" ) ),
"bounce_rate" => $stats_count > 0 ? array_sum( array_column( $stats, "bounce_rate" ) ) / $stats_count : 0,
"avg_duration" => $stats_count > 0 ? array_sum( array_column( $stats, "avg_duration" ) ) / $stats_count : 0,
],
"items" => $stats
];
return $response;
}
public function top_pages( $environment = "production", $before = "", $after = "", $limit = 10 ) {
if ( empty( $after ) ) {
$after = time();
}
if ( empty( $before ) ) {
$before = strtotime( "-30 days" );
}
$before = date( 'Y-m-d H:i:s', $before );
$after = date( 'Y-m-d H:i:s', $after );
$environments = self::environments();
foreach( $environments as $e ) {
if ( strtolower( $e->environment ) == strtolower( $environment ) ) {
$selected_environment = $e;
}
}
if ( empty( $selected_environment ) ) {
return [];
}
$fathom_ids = array_column( $selected_environment->fathom_analytics, "code" );
if ( empty( $fathom_ids ) ) {
return [];
}
$fathom_id = $fathom_ids[0];
$url = "https://api.usefathom.com/v1/aggregations?entity=pageview&entity_id={$fathom_id}&aggregates=visits,uniques,pageviews&field_grouping=pathname&date_from={$before}&date_to={$after}&sort_by=pageviews:desc&limit={$limit}";
$response = wp_remote_get( $url, [
"headers" => [ "Authorization" => "Bearer " . \CaptainCore\Providers\Fathom::credentials("api_key") ],
] );
if ( is_wp_error( $response ) ) {
return [];
}
$pages = json_decode( $response['body'] );
if ( ! is_array( $pages ) ) {
return [];
}
return $pages;
}
public function top_referrers( $environment = "production", $before = "", $after = "", $limit = 10 ) {
if ( empty( $after ) ) {
$after = time();
}
if ( empty( $before ) ) {
$before = strtotime( "-30 days" );
}
$before = date( 'Y-m-d H:i:s', $before );
$after = date( 'Y-m-d H:i:s', $after );
$environments = self::environments();
foreach( $environments as $e ) {
if ( strtolower( $e->environment ) == strtolower( $environment ) ) {
$selected_environment = $e;
}
}
if ( empty( $selected_environment ) ) {
return [];
}
$fathom_ids = array_column( $selected_environment->fathom_analytics, "code" );
if ( empty( $fathom_ids ) ) {
return [];
}
$fathom_id = $fathom_ids[0];
$url = "https://api.usefathom.com/v1/aggregations?entity=pageview&entity_id={$fathom_id}&aggregates=visits,uniques,pageviews&field_grouping=referrer_hostname&date_from={$before}&date_to={$after}&sort_by=visits:desc&limit={$limit}";
$response = wp_remote_get( $url, [
"headers" => [ "Authorization" => "Bearer " . \CaptainCore\Providers\Fathom::credentials("api_key") ],
] );
if ( is_wp_error( $response ) ) {
return [];
}
$referrers = json_decode( $response['body'] );
if ( ! is_array( $referrers ) ) {
return [];
}
return $referrers;
}
public function update_logs( $environment = "both" ) {
$command = "update-log list {$this->site_id}-$environment";
$response = Run::CLI( $command );
$update_logs = json_decode( $response );
if ( json_last_error() != JSON_ERROR_NONE ) {
return [];
}
foreach( $update_logs as $key => $update_log ) {
$update_logs[ $key ]->plugins = [];
$update_logs[ $key ]->themes = [];
$update_logs[ $key ]->core = $update_log->core;
$update_logs[ $key ]->view_quicksave = false;
$update_logs[ $key ]->view_changes = false;
$update_logs[ $key ]->view_files = [];
$update_logs[ $key ]->filtered_files = [];
$update_logs[ $key ]->response = [];
$update_logs[ $key ]->loading = true;
$update_logs[ $key ]->search = "";
}
return $update_logs;
}
public function users() {
$db_environments = new Environments();
$environments = $db_environments->fetch_environments( $this->site_id );
$results = (object) [];
foreach( $environments as $environment ) {
$users = empty( $environment->users ) ? [] : json_decode( $environment->users );
array_multisort(
array_column($users, 'roles'), SORT_ASC,
array_column($users, 'user_login'), SORT_ASC,
$users
);
if ( $users != "" ) {
$results->{$environment->environment} = $users;
}
}
return $results;
}
public function snapshots() {
$db_environments = new Environments();
$environments = $db_environments->fetch_environments( $this->site_id );
$results = (object) [];
foreach ($environments as $environment) {
$snapshots = ( new Snapshots )->fetch_environment( $this->site_id, $environment->environment_id );
foreach( $snapshots as $snapshot ) {
$snapshot->created_at = strtotime( $snapshot->created_at );
if ( $snapshot->user_id == 0 ) {
$user_name = "System";
} else {
$user_name = get_user_by( 'id', $snapshot->user_id )->display_name;
}
$snapshot->user = (object) [
"user_id" => $snapshot->user_id,
"name" => $user_name
];
unset( $snapshot->user_id );
}
$results->{$environment->environment} = $snapshots;
}
return $results;
}
public function quicksaves( $environment = "both" ) {
$command = "quicksave list {$this->site_id}-$environment";
$response = Run::CLI( $command );
$quicksaves = json_decode( $response );
if ( json_last_error() != JSON_ERROR_NONE ) {
return [];
}
foreach( $quicksaves as $key => $quicksave ) {
$quicksaves[ $key ]->plugins = [];
$quicksaves[ $key ]->themes = [];
$quicksaves[ $key ]->core = $quicksave->core ?? '';
$quicksaves[ $key ]->status = "";
$quicksaves[ $key ]->view_changes = false;
$quicksaves[ $key ]->view_files = [];
$quicksaves[ $key ]->filtered_files = [];
$quicksaves[ $key ]->loading = true;
$quicksaves[ $key ]->sandbox_loading = false;
$quicksaves[ $key ]->include_database = false;
$quicksaves[ $key ]->search = "";
}
return $quicksaves;
}
public function process_logs() {
$Parsedown = new \Parsedown();
$process_log = new ProcessLogs();
$process_logs = [];
$results = ( new ProcessLogSite )->fetch_process_logs( [ "site_id" => $this->site_id ] );
foreach ( $results as $result ) {
$item = $process_log->get( $result->process_log_id );
$item->created_at = strtotime( $item->created_at );
$item->name = $result->name;
$item->description_raw = $item->description;
$item->description = $Parsedown->text( $item->description );
$item->author = get_the_author_meta( 'display_name', $item->user_id );
$item->author_avatar = "https://www.gravatar.com/avatar/" . md5( get_the_author_meta( 'email', $item->user_id ) ) . "?s=80&d=mp";
$item->files = ( new ProcessLog( $item->process_log_id ) )->files();
$process_logs[] = $item;
}
return $process_logs;
}
public function update_details() {
$site = Sites::get( $this->site_id );
if ( $site == "" ) {
$response['response'] = 'Error: Site ID not found.';
return $response;
}
$environments = self::environments();
$details = json_decode( $site->details );
// Initialize totals
$total_visits = 0;
$total_storage = 0;
// Loop through environments and sum up only production usage
foreach ( $environments as $environment ) {
if ( $environment->environment == "Production" ) {
$total_visits += (int) $environment->visits;
$total_storage += (int) $environment->storage;
}
}
$details->visits = $total_visits;
$details->storage = $total_storage;
$details->username = $environments[0]->username;
Sites::update( [ "details" => json_encode( $details ) ], [ "site_id" => $site->site_id ] );
Sites::update_environments_cache( $this->site_id );
}
}