captaincore-manager/app/Providers/Kinsta.php
2026-04-10 10:47:30 -04:00

1595 lines
63 KiB
PHP

<?php
namespace CaptainCore\Providers;
class Kinsta {
public static function credentials( $record = "", $provider_id = "" ) {
$credentials = ( new \CaptainCore\Provider( "kinsta" ) )->credentials();
if ( ! empty( $provider_id ) ) {
$provider = \CaptainCore\Providers::get( $provider_id );
$credentials = ! empty( $provider->credentials ) ? json_decode( $provider->credentials ) : [];
}
if ( $record == "" ) {
return $credentials;
}
foreach( $credentials as $credential ) {
if ( $credential->name == $record ) {
return $credential->value;
}
}
}
public static function list() {
$providers = [];
$user = (object) ( new \CaptainCore\User )->fetch();
if ( ( new \CaptainCore\User )->is_admin() ) {
$providers = \CaptainCore\Providers::where( ["provider" => "kinsta" ] );
}
if ( empty( $providers ) ) {
$providers_admin = \CaptainCore\Providers::where( [ "provider" => "kinsta", "user_id" => "0" ] );
$providers_user = \CaptainCore\Providers::where( [ "provider" => "kinsta", "user_id" => $user->user_id ] );
$providers = array_merge($providers_admin, $providers_user);
}
$filteredProviders = array_map(function($provider) {
$name = $provider->name . " " . $provider->provider;
return [
'provider_id' => $provider->provider_id,
'provider' => $provider->provider,
'name' => $provider->name
];
}, $providers);
return $filteredProviders;
}
public static function list_sites( $record = "", $provider_id = "" ) {
$providers = self::list();
$providers_with_sites = [];
foreach( $providers as $provider ) {
if ( $provider['provider_id'] == 1 ) {
continue;
}
$api_key = self::credentials("api", $provider['provider_id']);
\CaptainCore\Remote\Kinsta::setApiKey( $api_key );
$response = \CaptainCore\Remote\Kinsta::get( "sites?company=". self::credentials("company_id", $provider['provider_id']) );
$sites = ! empty( $response->company->sites ) ? $response->company->sites : [];
$providers_with_sites[$provider['provider_id']] = $sites;
}
return $providers_with_sites;
}
public static function fetch_remote_sites( $provider_id = "" ) {
$api_key = self::credentials( "api", $provider_id );
$company_id = self::credentials( "company_id", $provider_id );
if ( empty( $api_key ) || empty( $company_id ) ) {
return [];
}
\CaptainCore\Remote\Kinsta::setApiKey( $api_key );
$response = \CaptainCore\Remote\Kinsta::get( "sites?company={$company_id}" );
if ( empty( $response->company->sites ) ) {
return [];
}
$sites = [];
foreach ( $response->company->sites as $kinsta_site ) {
$sites[] = [
'remote_id' => $kinsta_site->id,
'name' => $kinsta_site->display_name,
'label' => $kinsta_site->display_name,
'slug' => $kinsta_site->name ?? '',
'status' => $kinsta_site->status ?? 'active',
];
}
return $sites;
}
/**
* Fetch all environments for a remote Kinsta site, normalized for local use.
*
* Returns one entry per environment with the same field names used in
* wp_captaincore_environments (Production/Staging label, address, port,
* username, password, home_directory) so callers can diff against local rows
* directly.
*
* @param string $remote_site_id Kinsta site UUID
* @param string $provider_id CaptainCore provider id (defaults to primary Kinsta)
* @return array|\WP_Error
*/
public static function fetch_remote_environments( $remote_site_id = "", $provider_id = "" ) {
if ( empty( $remote_site_id ) ) {
return new \WP_Error( 'kinsta_missing_remote_id', 'Remote Kinsta site id is required' );
}
$api_key = self::credentials( "api", $provider_id );
if ( empty( $api_key ) ) {
return new \WP_Error( 'kinsta_missing_api_key', 'Kinsta API key not configured for this provider' );
}
\CaptainCore\Remote\Kinsta::setApiKey( $api_key );
$site_response = \CaptainCore\Remote\Kinsta::get( "sites/{$remote_site_id}" );
if ( empty( $site_response->site ) ) {
$message = $site_response->message ?? 'Kinsta site not found';
return new \WP_Error( 'kinsta_site_not_found', "Kinsta site lookup failed: {$message}" );
}
$username = $site_response->site->name ?? '';
$environments_response = \CaptainCore\Remote\Kinsta::get( "sites/{$remote_site_id}/environments" );
if ( empty( $environments_response->site->environments ) ) {
return new \WP_Error( 'kinsta_no_environments', 'No environments returned from Kinsta' );
}
$environments = [];
foreach ( $environments_response->site->environments as $env ) {
$label = ( $env->name === 'live' ) ? 'Production' : 'Staging';
$remote = [
'remote_id' => $env->id,
'environment' => $label,
'address' => $env->ssh_connection->ssh_ip->external_ip ?? '',
'port' => (string) ( $env->ssh_connection->ssh_port ?? '' ),
'username' => $username,
'password' => '',
'home_directory' => $env->web_root ?? '',
];
$password_response = \CaptainCore\Remote\Kinsta::get( "sites/environments/{$env->id}/ssh/password" );
$remote['password'] = $password_response->environment->sftp_password ?? '';
$environments[] = $remote;
}
return $environments;
}
public static function enrich_imported_site( $site_id, $remote_site, $provider_id = "" ) {
$remote_site = (object) $remote_site;
$api_key = self::credentials( "api", $provider_id );
\CaptainCore\Remote\Kinsta::setApiKey( $api_key );
$site_response = \CaptainCore\Remote\Kinsta::get( "sites/{$remote_site->remote_id}" );
$username = $site_response->site->name ?? '';
$response = \CaptainCore\Remote\Kinsta::get( "sites/{$remote_site->remote_id}/environments" );
if ( empty( $response->site->environments ) ) {
return;
}
$company_id = self::credentials( "company_id", $provider_id );
foreach ( $response->site->environments as $kinsta_environment ) {
if ( $kinsta_environment->name == "live" ) {
$address = $kinsta_environment->ssh_connection->ssh_ip->external_ip ?? '';
$port = $kinsta_environment->ssh_connection->ssh_port ?? '';
$home_url = $kinsta_environment->primaryDomain->name ?? '';
$home_directory = $kinsta_environment->web_root ?? '';
$core = $kinsta_environment->wordpress_version ?? '';
$password_response = \CaptainCore\Remote\Kinsta::get( "sites/environments/{$kinsta_environment->id}/ssh/password" );
$password = $password_response->environment->sftp_password ?? '';
$visits = '';
$timeframe_end = gmdate( 'Y-m-d' ) . 'T23:59:59.999Z';
$timeframe_start = gmdate( 'Y-m-d', strtotime( '-30 days' ) ) . 'T00:00:00.000Z';
$visits_response = \CaptainCore\Remote\Kinsta::get( "sites/environments/{$kinsta_environment->id}/analytics/visits?company_id={$company_id}&timeframe_start={$timeframe_start}&timeframe_end={$timeframe_end}" );
if ( ! empty( $visits_response->analytics->analytics_response->data[0]->total ) ) {
$visits = $visits_response->analytics->analytics_response->data[0]->total;
}
$environments = ( new \CaptainCore\Environments )->where( [ "site_id" => $site_id ] );
foreach ( $environments as $env ) {
if ( $env->environment === 'Production' ) {
( new \CaptainCore\Environments )->update( [
'address' => $address,
'username' => $username,
'password' => $password,
'port' => $port,
'home_url' => $home_url,
'home_directory' => $home_directory,
'core' => $core,
'visits' => $visits,
], [ 'environment_id' => $env->environment_id ] );
}
}
break;
}
}
}
public static function update_token( $token = "", $provider_id = "" ) {
if ( ! empty( $provider_id ) ) {
$provider = \CaptainCore\Providers::get( $provider_id );
} else {
$provider = ( new \CaptainCore\Provider( "kinsta" ) )->get();
}
$credentials = self::credentials( "", $provider_id );
foreach( $credentials as $credential ) {
if ( $credential->name == "token" ) {
$credential->value = $token;
}
}
( new \CaptainCore\Providers )->update( [ "credentials" => json_encode( $credentials ) ], [ "provider_id" => $provider->provider_id ] );
return self::verify( $provider_id );
}
public static function verify( $provider_id = "" ) {
$token = self::credentials( "token", $provider_id );
$data = [
'timeout' => 45,
'headers' => [
'Content-Type' => 'application/json',
'X-Token' => "$token",
],
'body' => json_encode( [
"variables" => [
"lang" => "en"
],
"operationName" => "KinstaBlog",
"query" => 'query KinstaBlog($lang: String!) {
kinstablog(lang: $lang) {
link
title
authorName
featuredImage
date
__typename
}
}'
] )
];
$response = wp_remote_post( "https://graphql-router.kinsta.com", $data );
if ( is_wp_error( $response ) ) {
return false;
}
$response = json_decode( $response['body'] );
if ( ! empty ( $response->errors ) ) {
return false;
}
if ( empty ( $response->data->kinstablog ) ) {
return false;
}
return true;
}
public static function new_site( $site ) {
$user = ( new \CaptainCore\User )->profile();
$token = self::credentials("token");
$company_id = self::credentials("company_id");
$username = self::credentials("username");
if ( ! empty( $site->provider_id ) ) {
$api_key = self::credentials("api", $site->provider_id);
$company_id = self::credentials("company_id", $site->provider_id);
$username = self::credentials("username", $site->provider_id);
\CaptainCore\Remote\Kinsta::setApiKey( $api_key );
}
$site = (object) $site;
if ( ! empty( $site->clone_site_id ) ) {
$environments = \CaptainCore\Remote\Kinsta::get( "sites/{$site->clone_site_id}/environments" );
foreach( $environments->site->environments as $environment ) {
if ( $environment->name == "live" ) {
$environment_id = $environment->id;
}
}
$environment = $environments->site->environments[0]->id;
$new_site = [
"company" => $company_id,
"display_name" => $site->name,
"source_env_id" => $environment_id,
];
$response = \CaptainCore\Remote\Kinsta::post( "sites/clone", $new_site );
$site->command = "new-site";
$site->intial_response = $response;
$site->message = "Creating site $site->name at Kinsta via site clone";
self::add_action( $response->operation_id, $site );
return $response->operation_id;
}
$new_site = [
"company" => $company_id,
"display_name" => $site->name,
"region" => $site->datacenter,
"is_subdomain_multisite" => false,
"install_mode" => "new",
"admin_email" => get_option( 'admin_email' ),
"admin_password" => substr ( str_shuffle( "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" ), 0, 16 ),
"admin_user" => $username,
"is_multisite" => false,
"site_title" => $site->name,
"woocommerce" => false,
"wordpressseo" => false,
"wp_language" => "en_US"
];
$response = \CaptainCore\Remote\Kinsta::post( "sites", $new_site );
$site->command = "new-site";
$site->intial_response = $response;
$site->message = "Creating site $site->name at Kinsta datacenter $site->datacenter";
self::add_action( $response->operation_id, $site );
return $response->operation_id;
}
public static function environments( $kinsta_id ) {
$response = \CaptainCore\Remote\Kinsta::get( "sites/$kinsta_id/environments" );
return $response->site->environments;
}
public static function connect_staging( $site_id ) {
$site = ( new \CaptainCore\Sites )->get( $site_id );
$environments = ( new \CaptainCore\Environments )->where( [ "site_id" => $site_id ] );
if ( empty( $site->provider_site_id ) ) {
return;
}
foreach( $environments as $environment ) {
if ( $environment->environment == "Production" ) {
$production_environment = $environment;
}
}
$time_now = date("Y-m-d H:i:s");
$response = \CaptainCore\Remote\Kinsta::get( "sites/{$site->provider_site_id}/environments" );
foreach( $response->site->environments as $kinsta_environment ) {
if ( $kinsta_environment->name == "staging" ) {
$new_environment = [
'site_id' => $site_id,
'created_at' => $time_now,
'updated_at' => $time_now,
'environment' => "Staging",
'address' => $kinsta_environment->ssh_connection->ssh_ip->external_ip,
'username' => $production_environment->username,
'password' => $production_environment->password,
'protocol' => $production_environment->protocol,
'port' => $kinsta_environment->ssh_connection->ssh_port,
'home_directory' => $production_environment->home_directory,
'database_username' => $production_environment->database_username,
'database_password' => "",
'offload_enabled' => "",
'offload_access_key' => "",
'offload_secret_key' => "",
'offload_bucket' => "",
'offload_path' => "",
'monitor_enabled' => 0,
'updates_enabled' => 1,
'updates_exclude_plugins' => "",
'updates_exclude_themes' => "",
];
( new \CaptainCore\Environments )->insert( $new_environment );
\CaptainCore\Run::CLI("site sync $site_id");
}
}
}
public static function sync_environments( $site_id ) {
$site = ( new \CaptainCore\Sites )->get( $site_id );
$environments = ( new \CaptainCore\Environments )->where( [ "site_id" => $site_id ] );
if ( empty( $site->provider_site_id ) ) {
return;
}
foreach( $environments as $environment ) {
if ( $environment->environment == "Production" ) {
$production_environment = $environment;
}
}
$response = \CaptainCore\Remote\Kinsta::get( "sites/{$site->provider_site_id}/environments" );
foreach( $response->site->environments as $kinsta_environment ) {
if ( $kinsta_environment->name == "live" ) {
foreach( $environments as $environment ) {
if ( $environment->environment == "Production" ) {
( new \CaptainCore\Environments )->update( [
"address" => $kinsta_environment->ssh_connection->ssh_ip->external_ip,
"port" => $kinsta_environment->ssh_connection->ssh_port,
], [ "environment_id" => $environment->environment_id ] );
}
}
}
if ( $kinsta_environment->name == "staging" ) {
foreach( $environments as $environment ) {
if ( $environment->environment == "Staging" ) {
( new \CaptainCore\Environments )->update( [
"address" => $kinsta_environment->ssh_connection->ssh_ip->external_ip,
"port" => $kinsta_environment->ssh_connection->ssh_port,
"username" => $production_environment->username,
"password" => $production_environment->password,
"protocol" => $production_environment->protocol,
"home_directory" => $production_environment->home_directory,
], [ "environment_id" => $environment->environment_id ]);
}
}
}
}
}
public static function create_staging_and_deploy( $site_id ) {
$site = ( new \CaptainCore\Sites )->get( $site_id );
if ( empty( $site->provider_site_id ) ) {
return;
}
$environments = self::environments( $site->provider_site_id );
$environment_production_id = "";
foreach( $environments as $environment ) {
if ( $environment->name == "live" ) {
$environment_production_id = $environment->id;
}
}
$data = [
"display_name" => "Staging",
"is_premium" => false,
"source_env_id" => $environment_production_id
];
$response = \CaptainCore\Remote\Kinsta::post( "sites/{$site->provider_site_id}/environments/clone", $data );
$action = (object) [
"command" => "deploy-to-staging",
"step" => 2,
"message" => "Deploying $site->name to staging environment",
"name" => $site->name,
"site_id" => $site_id,
"kinsta_site_id" => $site->provider_site_id,
"environment_production_id" => $environment_production_id,
];
self::add_action( $response->operation_id, $action );
return $response->operation_id;
}
public static function deploy_to_staging( $site_id ) {
$site = ( new \CaptainCore\Sites )->get( $site_id );
if ( empty( $site->provider_site_id ) ) {
return;
}
$environments = self::environments( $site->provider_site_id );
$api_key = self::credentials("api");
$data = [
"tag" => "Deploy to staging from API"
];
$environment_production_id = "";
$environment_staging_id = "";
foreach( $environments as $environment ) {
if ( $environment->name == "live" ) {
$environment_production_id = $environment->id;
}
if ( $environment->name == "staging" ) {
$environment_staging_id = $environment->id;
}
}
// If no staging then create that
if ( empty( $environment_staging_id ) ) {
return self::create_staging_and_deploy( $site_id );
}
$response = \CaptainCore\Remote\Kinsta::post( "sites/environments/$environment_production_id/manual-backups", $data );
if ( empty ( $response->operation_id ) ) {
return false;
}
$connect_staging = false;
$environments = ( new \CaptainCore\Site( $site_id, true ) )->environments();
if ( count( $environments ) == 1 ) {
$connect_staging = true;
}
$action = (object) [
"command" => "deploy-to-staging",
"step" => 1,
"message" => "Deploying $site->name to staging environment",
"name" => $site->name,
"site_id" => $site_id,
"connect_staging" => $connect_staging,
"kinsta_site_id" => $site->provider_site_id,
"environment_production_id" => $environment_production_id,
"environment_staging_id" => $environment_staging_id,
];
self::add_action( $response->operation_id, $action );
return $response->operation_id;
}
public static function deploy_to_production( $site_id ) {
$site = ( new \CaptainCore\Sites )->get( $site_id );
if ( empty( $site->provider_site_id ) ) {
return;
}
$environments = self::environments( $site->provider_site_id );
$api_key = self::credentials("api");
$environment_production_id = "";
$environment_staging_id = "";
foreach( $environments as $environment ) {
if ( $environment->name == "live" ) {
$environment_production_id = $environment->id;
}
if ( $environment->name == "staging" ) {
$environment_staging_id = $environment->id;
}
}
$data = [
"source_env_id" => $environment_staging_id,
"target_env_id" => $environment_production_id,
"push_db" => true,
"push_files" => true,
"run_search_and_replace" => true
];
$response = \CaptainCore\Remote\Kinsta::put( "sites/{$site->provider_site_id}/environments", $data );
if ( empty ( $response->operation_id ) ) {
return false;
}
$name = "";
$environments = ( new \CaptainCore\Site( $site_id, true ) )->environments();
foreach( $environments as $environment ) {
if ( $environment->environment == "Staging") {
$name = $environment->home_url;
}
}
$action = (object) [
"command" => "deploy-to-production",
"message" => "Deploying $name to production environment",
"name" => $name,
"site_id" => $site_id,
"kinsta_site_id" => $site->provider_site_id,
];
self::add_action( $response->operation_id, $action );
return $response->operation_id;
}
public static function provider_sync() {
$data = [
'timeout' => 45,
'headers' => [
'Content-Type' => 'application/json',
'X-Token' => self::credentials("token")
],
'body' => json_encode( [
"variables" => [
"idCompany" => self::credentials("company_id")
],
"query" => 'query ExportSites($idCompany: String!) {
company(id: $idCompany) {
id
name
sites {
id
name
path
createdAt
environment(name: "live") {
id
primaryDomain {
name
}
}
}
}
}'
] )
];
$response = wp_remote_post( "https://graphql-router.kinsta.com", $data );
$response = json_decode( $response['body'] );
$company_id = self::credentials("company_id");
$sites = ( new \CaptainCore\Sites )->where( [ "provider" => "kinsta" ] );
foreach( $sites as $site ) {
foreach( $response->data->company->sites as $kinsta_site ) {
$site_name = $kinsta_site->environment->primaryDomain->name;
$site_name = str_replace( "www.", "", $site_name );
if ( $site->name == $site_name ) {
( new \CaptainCore\Sites )->update( [ "provider_site_id" => $kinsta_site->id ], [ "site_id" => $site->site_id ] );
}
}
}
}
public static function actions() {
$provider = ( new \CaptainCore\Provider( "kinsta" ) )->get();
$details = $provider->details;
$details = empty( $details ) ? (object) [ "actions" => [] ] : json_decode( $details );
return $details->actions;
}
public static function add_action( $action_id = 0, $action = "" ) {
$provider = ( new \CaptainCore\Provider( "kinsta" ) )->get();
if ( ! empty( $action->provider_id ) ) {
$provider = \CaptainCore\Providers::get( $action->provider_id );
}
$user_id = ( new \CaptainCore\User )->user_id();
$time_now = date("Y-m-d H:i:s");
$provider_action = [
"user_id" => $user_id,
"provider_id" => $provider->provider_id,
"provider_key" => $action_id,
"status" => "started",
"action" => json_encode( $action ),
'created_at' => $time_now,
'updated_at' => $time_now,
];
$provider_action_id = ( new \CaptainCore\ProviderActions )->insert( $provider_action );
return;
}
public static function action_check( $provider_action_id = 0, $return_response = false ) {
$provider_action = ( new \CaptainCore\ProviderActions )->get( $provider_action_id );
if ( empty( $provider_action ) ) {
return "";
}
$action = json_decode( $provider_action->action );
$token = self::credentials("token");
$data = [
'timeout' => 45,
'headers' => [
'Content-Type' => 'application/json',
'X-Token' => "$token",
],
'body' => json_encode( [
"variables" => [
"idAction" => (int) ( $provider_action->provider_key ?? 0 )
],
"operationName" => "Action",
"query" => 'query Action($idAction: Int!) {
action(id: $idAction) {
error
isDone
__typename
}
action_liveKeys(id: $idAction)
}'
] )
];
$response = wp_remote_post( "https://graphql-router.kinsta.com", $data );
if ( is_wp_error( $response ) ) {
return 404;
}
$response = json_decode( $response['body'] );
if ( ! empty ( $response->errors ) ) {
return 404;
}
if ( empty ( $response->data->action ) ) {
return 202;
}
if ( $return_response ) {
return $response;
}
if ( $response->data->action->isDone ) {
return 200;
}
return 202;
}
public static function fetch_sftp_password( $site_key = 0, $provider_id = "" ) {
$api_key = self::credentials( "api", $provider_id );
\CaptainCore\Remote\Kinsta::setApiKey( $api_key );
$response = \CaptainCore\Remote\Kinsta::get( "sites/{$site_key}/environments" );
if ( empty( $response->site->environments ) ) {
return '';
}
foreach ( $response->site->environments as $env ) {
if ( $env->name == "live" ) {
$password_response = \CaptainCore\Remote\Kinsta::get( "sites/environments/{$env->id}/ssh/password" );
return $password_response->environment->sftp_password ?? '';
}
}
return '';
}
public static function fetch_site_details( $site_key = 0 ) {
$token = self::credentials("token");
$data = [
'timeout' => 45,
'headers' => [
'Content-Type' => 'application/json',
'X-Token' => "$token",
],
'body' => json_encode( [
"variables" => [
"idSite" => $site_key,
],
"operationName" => "getHeaderSite",
"query" => 'fragment HeaderEnvironment on Environment {
id
name
displayName
isPremium
isBlocked
__typename
}
query getHeaderSite($idSite: String!) {
site(id: $idSite) {
id
displayName
environments {
...HeaderEnvironment
__typename
}
environments_liveKeys
__typename
}
}'
] )
];
$response = wp_remote_post( "https://graphql-router.kinsta.com", $data );
$response = json_decode( $response['body'] );
foreach( $response->data->site->environments as $env ) {
if ( $env->name == "live" ) {
$environment = $env;
}
}
// te: String!, $idEnvironment: String) {\n site(id: $idSite) {\n id\n name\n displayName\n dbName\n path\n usr\n siteLabels {\n id\n name\n __typename\n }\n company {\n id\n name\n __typename\n }\n environment(id: $idEnvironment) {\n id\n isPremium\n cloudflareIP\n customHostnames {\n id\n status\n __typename\n }\n phpWorkerLimit\n mysqlEditorDomain {\n id\n name\n __typename\n }\n activeContainer {\n id\n lxdSshPort\n loadBalancer {\n id\n extIP\n __typename\n }\n lxdServer {\n id\n extIP\n intHostname\n zone {\n id\n name\n __typename\n }\n __typename\n }\n __typename\n }\n customHostnames_liveKeys\n activeContainer_liveKeys\n __typename\n }\n hasPendingTransfer\n hasFreeEnvSlot\n siteLabels_liveKeys\n environment_liveKeys(id: $idEnvironment)\n hasPendingTransfer_liveKeys\n hasFreeEnvSlot_liveKeys\n __typename\n }\n}\n"}
$data = [
'timeout' => 45,
'headers' => [
'Content-Type' => 'application/json',
'X-Token' => "$token",
],
'body' => json_encode( [
"variables" => [
"idSite" => $site_key,
"idEnvironment" => $environment->id
],
"operationName" => "SiteDetails",
"query" => 'query SiteDetails($idSite: String!, $idEnvironment: String) {
site(id: $idSite) {
id
name
displayName
dbName
path
usr
siteLabels {
id
name
__typename
}
company {
id
name
__typename
}
environment(id: $idEnvironment) {
id
isPremium
cloudflareIP
customHostnames {
id
status
__typename
}
phpWorkerLimit
mysqlEditorDomain {
id
name
__typename
}
activeContainer {
id
lxdSshPort
loadBalancer {
id
extIP
__typename
}
lxdServer {
id
extIP
intHostname
zone {
id
name
__typename
}
__typename
}
__typename
}
customHostnames_liveKeys
activeContainer_liveKeys
__typename
}
hasPendingTransfer
hasFreeEnvSlot
siteLabels_liveKeys
environment_liveKeys(id: $idEnvironment)
hasPendingTransfer_liveKeys
hasFreeEnvSlot_liveKeys
__typename
}
}'
] )
];
$response = wp_remote_post( "https://graphql-router.kinsta.com", $data );
$response = json_decode( $response['body'] );
return $response->data->site;
}
public static function action_result( $provider_key = 0 ) {
$provider_key = (int) $provider_key;
$token = self::credentials("token");
$data = [
'timeout' => 45,
'headers' => [
'Content-Type' => 'application/json',
'X-Token' => "$token",
],
'body' => json_encode( [
"variables" => [
"idAction" => $provider_key
],
"operationName" => "Action",
"query" => 'query Action($idAction: Int!) {
action(id: $idAction) {
error
result
__typename
}
}'
] )
];
$response = wp_remote_post( "https://graphql-router.kinsta.com", $data );
if ( is_wp_error( $response ) ) {
return false;
}
$response = json_decode( $response['body'] );
if ( ! empty ( $response->errors ) ) {
return false;
}
if ( empty ( $response->data->action ) ) {
return false;
}
return $response->data->action->result;
}
public static function company_users() {
$company_id = self::credentials("company_id");
$token = self::credentials("token");
$data = [
'timeout' => 45,
'headers' => [
'Content-Type' => 'application/json',
'X-Token' => "$token",
],
'body' => json_encode( [
"variables" => [
"idCompany" => $company_id
],
"operationName" => "CompanyUsers",
"query" => 'fragment CompanyCapabilities on Capability {
idCompany
idSite
idRole
type
permissions {
type
key
idCompany
idSite
__typename
}
__typename
}
fragment CompanyUsersItem on CompanyUser {
id
user {
id
email
image
fullName
has2FA
isKinstaStaff
intercomConversationUrl(cacheControl: CACHED_OR_RENEW)
intercomConversationUrl_liveKeys(cacheControl: CACHED_OR_RENEW)
__typename
}
capabilities {
...CompanyCapabilities
__typename
}
capabilities_liveKeys
__typename
}
fragment PendingInvitationsFragment on Invitation {
id
email
capabilities {
...CompanyCapabilities
__typename
}
__typename
}
fragment CompanyUser on User {
id
email
image
fullName
isKinstaStaff
__typename
}
query CompanyUsers($idCompany: String!) {
company(id: $idCompany) {
id
name
sites {
id
displayName
__typename
}
users {
...CompanyUsersItem
__typename
}
pendingInvitations {
...PendingInvitationsFragment
__typename
}
sites_liveKeys
users_liveKeys
pendingInvitations_liveKeys
__typename
}
user {
...CompanyUser
__typename
}
user_liveKeys
}'
] )
];
$response = wp_remote_post( "https://graphql-router.kinsta.com", $data );
if ( is_wp_error( $response ) ) {
return false;
}
$response = json_decode( $response['body'] );
if ( ! empty ( $response->errors ) ) {
return false;
}
if ( empty ( $response->data ) ) {
return false;
}
return $response->data;
}
/**
* Get list of Kinsta sites/environments eligible to be pushed to.
*/
public static function get_push_targets( $source_site_id, $source_environment_id ) {
global $wpdb; // Access the WordPress database object
// --- Get Source Site Info ---
$source_site = \CaptainCore\Sites::get( $source_site_id );
// Need provider_id for filtering targets to the same Kinsta account
$provider_id = $source_site->provider_id;
// --- Get Allowed Sites for Current User ---
$user_sites = new \CaptainCore\Sites(); // Instantiate to get user-specific permissions
$allowed_site_ids = $user_sites->site_ids(); // Get the array of site IDs the user can access
// If the user has no allowed sites, return early
if ( empty( $allowed_site_ids ) ) {
return [];
}
// --- Prepare Database Query ---
$sites_table = $wpdb->prefix . 'captaincore_sites';
$environments_table = $wpdb->prefix . 'captaincore_environments';
// Prepare base WHERE clauses safely
$where_clauses = [
$wpdb->prepare( "s.provider = %s", "kinsta" ),
$wpdb->prepare( "s.status = %s", "active" ),
$wpdb->prepare( "e.environment_id != %d", $source_environment_id ),
];
// Add provider_id filtering logic (ensures push targets are within the same Kinsta account)
if ( empty( $provider_id ) || $provider_id == '1' ) { // Treat provider_id 1 (often default/unmanaged) or empty as needing matching targets
$where_clauses[] = "( s.provider_id = '1' OR s.provider_id IS NULL )";
} else {
// If source has a specific Kinsta provider_id, target must match
$where_clauses[] = $wpdb->prepare( "s.provider_id = %s", $provider_id );
}
// --- Add User Permission Filter ---
// Safely create the IN clause for allowed site IDs
$allowed_ids_sql = implode( ',', array_map( 'intval', $allowed_site_ids ) ); // Ensure integers
$where_clauses[] = "s.site_id IN ( $allowed_ids_sql )"; // Add the IN clause
$where_sql = implode( ' AND ', $where_clauses );
// Construct the final optimized query string
// Note: $allowed_ids_sql is already sanitized by array_map('intval', ...)
$sql = "SELECT s.site_id, s.name, e.environment, e.environment_id, e.home_url
FROM $environments_table e
INNER JOIN $sites_table s ON e.site_id = s.site_id
WHERE $where_sql
ORDER BY s.name ASC";
$results = $wpdb->get_results( $sql ); // Execute the query
// Map results directly to the target format
$targets = [];
if ( $results ) {
foreach ( $results as $row ) {
$targets[] = [
'site_id' => (int) $row->site_id,
'name' => $row->name,
'environment' => $row->environment,
'environment_id' => (int) $row->environment_id,
'home_url' => $row->home_url ?? $row->name,
];
}
}
return $targets;
}
/**
* Pushes one Kinsta environment to another.
*
* @param int $source_environment_id The CaptainCore ID of the source environment.
* @param int $target_environment_id The CaptainCore ID of the target environment.
* @return object|WP_Error Kinsta API response object or WP_Error on failure.
*/
public static function push_environment( $source_environment_id, $target_environment_id ) {
// Get source env and site
$source_env = \CaptainCore\Environments::get( $source_environment_id );
$source_site = \CaptainCore\Sites::get( $source_env->site_id );
// Get target env and site
$target_env = \CaptainCore\Environments::get( $target_environment_id );
$target_site = \CaptainCore\Sites::get( $target_env->site_id );
if ( ! $source_site || ! $target_site || $source_site->provider_id != $target_site->provider_id ) {
return new \WP_Error('provider_mismatch', 'Source and target sites are not managed by the same provider account.', ['status' => 400]);
}
$api_key = self::credentials("api", $source_site->provider_id);
if ( ! $api_key ) {
return new \WP_Error('kinsta_api_key_missing', 'Kinsta API key not configured or set for the request.', ['status' => 500]);
}
\CaptainCore\Remote\Kinsta::setApiKey( $api_key );
// --- Fetch Source Kinsta Env ID ---
$source_kinsta_env_id = self::get_kinsta_env_id_from_cc_env( $source_site, $source_env );
if ( is_wp_error( $source_kinsta_env_id ) ) {
return $source_kinsta_env_id;
}
// --- Fetch Target Kinsta Env ID ---
$target_kinsta_env_id = self::get_kinsta_env_id_from_cc_env( $target_site, $target_env );
if ( is_wp_error( $target_kinsta_env_id ) ) {
return $target_kinsta_env_id;
}
$data = [
"source_env_id" => $source_kinsta_env_id,
"target_env_id" => $target_kinsta_env_id,
"push_db" => true,
"push_files" => true,
];
// Call Kinsta API
$response = \CaptainCore\Remote\Kinsta::put( "sites/{$source_site->provider_site_id}/environments", $data );
if ( ! $response || isset( $response->error ) || (isset($response->status) && $response->status >= 400) ) {
$error_message = $response->message ?? 'Unknown Kinsta API error during push.';
return new \WP_Error( 'kinsta_push_failed', $error_message, [ 'status' => $response->status ?? 500, 'details' => $response ] );
}
return $response;
}
/**
* Helper to get Kinsta Environment ID from a CaptainCore Environment object
*/
private static function get_kinsta_env_id_from_cc_env( $cc_site, $cc_env ) {
if ( ! $cc_site || ! $cc_env || empty( $cc_site->provider_site_id ) ) {
return new \WP_Error( 'invalid_input', 'Invalid CaptainCore site or environment object.' );
}
$kinsta_environments_response = \CaptainCore\Remote\Kinsta::get( "sites/{$cc_site->provider_site_id}/environments" );
if ( ! $kinsta_environments_response || isset( $kinsta_environments_response->error ) || ! isset( $kinsta_environments_response->site->environments ) ) {
return new \WP_Error( 'kinsta_api_error', "Could not fetch environments from Kinsta for site {$cc_site->name}.", [ 'status' => 500, 'details' => $kinsta_environments_response ] );
}
$cc_env_name_lower = strtolower( $cc_env->environment );
$kinsta_env_name_match = ( $cc_env_name_lower == 'production' ) ? 'live' : $cc_env_name_lower;
foreach ( $kinsta_environments_response->site->environments as $kinsta_env ) {
if ( strtolower( $kinsta_env->name ) == $kinsta_env_name_match ) {
return $kinsta_env->id; // Found it
}
}
return new \WP_Error( 'kinsta_env_not_found', "Could not find matching Kinsta environment for {$cc_site->name} ({$cc_env->environment}).", [ 'status' => 404 ] );
}
public static function invite_emails( $emails = [], $idSite = "" ) {
$users = self::company_users();
$token = self::credentials("token");
$company_id = self::credentials("company_id");
$user_ids = [];
$response = [];
foreach ( $users->company->users as $user ) {
if ( in_array( $user->user->email, $emails ) ) {
$capabilities = [];
$user_ids[] = $user->user->id;
foreach( $user->capabilities as $site ) {
$capabilities[] = (object) [
"idSite" => $site->idSite,
"idRole" => $site->idRole
];
}
// Add new site to shared access
$capabilities[] = (object) [
"idSite" => $idSite,
"idRole" => "site-admin"
];
$response[] = "Found {$user->user->email} adding the following capabilities";
$response[] = $capabilities;
}
}
$data = [
'timeout' => 45,
'headers' => [
'Content-Type' => 'application/json',
'X-Token' => "$token",
],
'body' => json_encode( [
"variables" => [
"idCompany" => $company_id,
"idUsers" => $user_ids,
"siteCapabilities" => $capabilities
],
"operationName" => "EditSiteCapabilities",
"query" => 'mutation EditSiteCapabilities($idCompany: String!, $idUsers: [String!]!, $siteCapabilities: [SiteCapability!]!) {
idAction: editSiteCapabilities(
idCompany: $idCompany
idUsers: $idUsers
siteCapabilities: $siteCapabilities
runActionInBackground: true
)
}'
] )
];
$response = wp_remote_post( "https://graphql-router.kinsta.com", $data );
if ( is_wp_error( $response ) ) {
return false;
}
$response = json_decode( $response['body'] );
if ( ! empty ( $response->errors ) ) {
return false;
}
if ( empty ( $response->idAction ) ) {
return false;
}
return $response->data->idAction;
}
/**
* Private helper to get the Kinsta Environment ID.
*/
private static function get_kinsta_env_id( $site_id, $environment_name ) {
$site = ( new \CaptainCore\Sites )->get( $site_id );
if ( ! $site || $site->provider != 'kinsta' || empty( $site->provider_site_id ) ) {
return new \WP_Error( 'not_kinsta_site', 'This is not a Kinsta-managed site.', [ 'status' => 400 ] );
}
// Use the site's provider_id if available, otherwise default
$api_key = self::credentials("api", $site->provider_id );
\CaptainCore\Remote\Kinsta::setApiKey( $api_key );
$response = \CaptainCore\Remote\Kinsta::get( "sites/{$site->provider_site_id}/environments" );
if ( ! $response || isset( $response->error ) || ! isset( $response->site->environments ) ) {
return new \WP_Error( 'kinsta_api_error', 'Could not fetch environments from Kinsta.', [ 'status' => 500, 'details' => $response ] );
}
foreach ( $response->site->environments as $env ) {
if ( strtolower( $env->name ) == strtolower( $environment_name ) ) {
return $env->id;
}
}
return new \WP_Error( 'env_not_found', 'The specified environment was not found on Kinsta.', [ 'status' => 404 ] );
}
/**
* Get phpMyAdmin login URL for a Kinsta environment.
*/
public static function get_phpmyadmin_url( $site_id, $env_name ) {
if ( strtolower( $env_name ) == "production" ) {
$env_name = "live";
}
$kinsta_env_id = self::get_kinsta_env_id( $site_id, $env_name );
if ( is_wp_error( $kinsta_env_id ) ) {
return $kinsta_env_id;
}
$site = ( new \CaptainCore\Sites )->get( $site_id );
$api_key = self::credentials("api", $site->provider_id );
\CaptainCore\Remote\Kinsta::setApiKey( $api_key );
$response = \CaptainCore\Remote\Kinsta::post( "sites/environments/{$kinsta_env_id}/pma-login-token" );
if ( ! $response || isset( $response->error ) || ! isset($response->url) ) {
return new \WP_Error( 'kinsta_api_error', 'Error generating phpMyAdmin URL.', [ 'status' => 500, 'details' => $response ] );
}
return $response->url;
}
/**
* Get list of domains for a Kinsta environment.
*/
public static function get_domains( $site_id, $environment = 'production' ) {
$kinsta_env_name = ( $environment == 'production' ) ? 'live' : $environment;
$site = \CaptainCore\Sites::get( $site_id );
$kinsta_site_id = $site->provider_site_id;
if ( empty( $kinsta_site_id ) ) {
return new \WP_Error( 'kinsta_missing_id', 'Kinsta Remote ID not set for this site.' );
}
// 1. Fetch Environments to find the specific Environment ID
$env_response = \CaptainCore\Remote\Kinsta::get( "sites/{$kinsta_site_id}/environments" );
if ( is_wp_error( $env_response ) ) {
return $env_response;
}
$env_id = null;
$environments = $env_response->site->environments ?? [];
foreach ( $environments as $env ) {
if ( strtolower( $env->name ) === strtolower( $kinsta_env_name ) ) {
$env_id = $env->id;
break;
}
}
if ( ! $env_id ) {
return new \WP_Error( 'kinsta_env_not_found', "Kinsta environment '{$kinsta_env_name}' not found." );
}
// 2. Fetch Domains for the specific environment
$response = \CaptainCore\Remote\Kinsta::get( "sites/environments/{$env_id}/domains" );
if ( is_wp_error( $response ) ) {
return $response;
}
$domains = $response->environment->site_domains ?? [];
// 3. Check verification status for each domain
foreach ( $domains as $key => $domain ) {
$is_active = true;
$verification_records = [];
if ( strpos( $domain->name, '*.' ) !== false ) {
unset( $domains[$key] );
continue;
}
if ( strpos( $domain->name, 'kinsta.cloud' ) !== false ) {
$is_active = true;
} else {
$verify_response = \CaptainCore\Remote\Kinsta::get( "sites/environments/domains/{$domain->id}/verification-records" );
if ( ! is_wp_error( $verify_response ) && ! empty( $verify_response->site_domain->verification_records ) ) {
$is_active = false;
// FIX: array_values() forces this to be a strictly indexed JSON array [{},{}]
// preventing Vue from seeing "empty" slots or object keys.
$verification_records = array_values( $verify_response->site_domain->verification_records );
}
}
$domains[$key]->is_active = $is_active;
$domains[$key]->verification_records = $verification_records;
// Auto-provision DNS records if zone exists in Constellix
if ( ! empty( $verification_records ) ) {
self::auto_provision_dns( $domain->name, $verification_records );
}
}
return array_values( $domains );
}
/**
* Auto-provision verification DNS records to Constellix zone if managed.
*/
private static function auto_provision_dns( $domain_name, $verification_records ) {
if ( ! defined( 'CONSTELLIX_API_KEY' ) || ! defined( 'CONSTELLIX_SECRET_KEY' ) ) {
return;
}
// Skip if already provisioned (cached for 1 day, keyed by domain + records hash)
$transient_key = 'captaincore_dns_prov_' . md5( $domain_name . json_encode( $verification_records ) );
if ( get_transient( $transient_key ) ) {
return;
}
// Get the apex domain
$apex_domain = ( new \CaptainCore\Domains )->get_domain( $domain_name );
// Look up Constellix zone
$constellix_domains = \CaptainCore\Remote\Constellix::all( 'domains' );
if ( empty( $constellix_domains ) ) {
return;
}
$zone_id = null;
foreach ( $constellix_domains as $item ) {
if ( $item->name === $apex_domain ) {
$zone_id = $item->id;
break;
}
}
if ( ! $zone_id ) {
return;
}
// Fetch existing records for duplicate checking
$existing_records = \CaptainCore\Remote\Constellix::get( "domains/{$zone_id}/records", [ 'perPage' => 100 ] );
$existing_data = $existing_records->data ?? [];
foreach ( $verification_records as $record ) {
if ( empty( $record->type ) || empty( $record->name ) || empty( $record->value ) ) {
continue;
}
$record_type = strtolower( $record->type );
$record_name = str_replace( '.' . $apex_domain, '', $record->name );
// Check for duplicates
$exists = false;
foreach ( $existing_data as $existing ) {
if ( strtolower( $existing->type ) === $record_type && $existing->name === $record_name ) {
if ( $record_type === 'txt' && ! empty( $existing->value ) ) {
// Check if this specific value already exists
foreach ( $existing->value as $val ) {
$clean_val = trim( str_replace( '"', '', $val->value ?? '' ) );
if ( $clean_val === $record->value ) {
$exists = true;
break;
}
}
// TXT record with same name exists but missing this value — append it
if ( ! $exists ) {
$updated_values = $existing->value;
$updated_values[] = (object) [ 'value' => $record->value, 'enabled' => true ];
$post = [
'type' => $record_type,
'name' => $record_name,
'ttl' => $existing->ttl ?? 3600,
'value' => $updated_values,
];
\CaptainCore\Remote\Constellix::put( "domains/{$zone_id}/records/{$existing->id}", $post );
$exists = true;
}
} else {
// CNAME or other type with same name already exists
$exists = true;
}
break;
}
}
if ( $exists ) {
continue;
}
// Create new record
$post = [
'type' => $record_type,
'name' => $record_name,
'ttl' => 3600,
'value' => [[ 'value' => $record->value, 'enabled' => true ]],
];
\CaptainCore\Remote\Constellix::post( "domains/{$zone_id}/records", $post );
}
// Mark as provisioned
set_transient( $transient_key, true, DAY_IN_SECONDS );
}
/**
* Add a domain to a Kinsta environment.
*/
public static function add_domain( $site_id, $env_name, $params ) {
if ( strtolower( $env_name ) == "production" ) {
$env_name = "live";
}
$kinsta_env_id = self::get_kinsta_env_id( $site_id, $env_name );
if ( is_wp_error( $kinsta_env_id ) ) {
return $kinsta_env_id;
}
$site = ( new \CaptainCore\Sites )->get( $site_id );
$api_key = self::credentials("api", $site->provider_id );
\CaptainCore\Remote\Kinsta::setApiKey( $api_key );
$body = [
'domain_name' => $params['domain_name'],
'is_wildcardless' => $params['is_wildcardless'] ?? false,
];
return \CaptainCore\Remote\Kinsta::post( "sites/environments/{$kinsta_env_id}/domains", $body );
}
/**
* Delete a domain from a Kinsta environment.
*/
public static function delete_domain( $site_id, $env_name, $params ) {
if ( strtolower( $env_name ) == "production" ) {
$env_name = "live";
}
$kinsta_env_id = self::get_kinsta_env_id( $site_id, $env_name );
if ( is_wp_error( $kinsta_env_id ) ) {
return $kinsta_env_id;
}
$site = \CaptainCore\Sites::get( $site_id );
$api_key = self::credentials("api", $site->provider_id );
\CaptainCore\Remote\Kinsta::setApiKey( $api_key );
$body = [
'domain_ids' => $params['domain_ids'], // Expecting an array
];
return \CaptainCore\Remote\Kinsta::delete( "sites/environments/{$kinsta_env_id}/domains", $body );
}
/**
* Set the primary domain for a Kinsta environment.
*/
public static function set_primary_domain( $site_id, $env_name, $params ) {
if ( strtolower( $env_name ) == "production" ) {
$env_name = "live";
}
$kinsta_env_id = self::get_kinsta_env_id( $site_id, $env_name );
if ( is_wp_error( $kinsta_env_id ) ) {
return $kinsta_env_id;
}
$site = ( new \CaptainCore\Sites )->get( $site_id );
$api_key = self::credentials("api", $site->provider_id );
\CaptainCore\Remote\Kinsta::setApiKey( $api_key );
$body = [
'domain_id' => $params['domain_id'],
'run_search_and_replace' => $params['run_search_and_replace'] ?? true,
];
return \CaptainCore\Remote\Kinsta::put( "sites/environments/{$kinsta_env_id}/change-primary-domain", $body );
}
/**
* Fetch verification records for a specific domain.
*/
public function get_verification_records( $domain_id ) {
$response = \CaptainCore\Remote\Kinsta::get( "sites/environments/domains/{$domain_id}/verification-records" );
if ( is_wp_error( $response ) ) {
return $response;
}
// Access as object: $response->site_domain->verification_records
return $response->site_domain->verification_records ?? [];
}
/**
* "Check" verification by re-fetching the domain details.
* Kinsta checks in the background, so we just need to pull the latest status.
*/
public function check_verification( $site_id, $domain_id ) {
$site = \CaptainCore\Sites::get( $site_id );
$kinsta_site_id = $site->provider_site_id;
// 1. Get Environment ID (assuming 'live')
$env_response = \CaptainCore\Remote\Kinsta::get( "sites/{$kinsta_site_id}/environments" );
if ( is_wp_error( $env_response ) ) return $env_response;
$env_id = null;
$environments = $env_response->site->environments ?? [];
foreach ( $environments as $env ) {
if ( $env->name === 'live' ) {
$env_id = $env->id;
break;
}
}
if ( ! $env_id ) return new \WP_Error( 'env_not_found', 'Kinsta environment not found.' );
// 2. Re-fetch domains to get latest status
$response = \CaptainCore\Remote\Kinsta::get( "sites/environments/{$env_id}/domains" );
if ( is_wp_error( $response ) ) return $response;
$domains = $response->site->domains ?? [];
// Find our specific domain object
foreach ( $domains as $domain ) {
if ( $domain->id === $domain_id ) {
// Map is_active status on this single object before returning
$status = $domain->status ?? '';
$domain->is_active = ( $status === 'live' );
return $domain;
}
}
return new \WP_Error( 'domain_not_found', 'Domain not found in Kinsta response.' );
}
}