mirror of
https://gh.wpcy.net/https://github.com/CaptainCore/captaincore-manager.git
synced 2026-04-24 09:42:14 +08:00
1713 lines
No EOL
72 KiB
PHP
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 );
|
|
}
|
|
|
|
} |