File "class-siterpc.php"
Full Path: /home/rfaztorb/public_html/wordpress/search/file/pomo/updraft/plugins-old/updraftcentral/classes/class-siterpc.php
File size: 23.61 KB
MIME-type: text/x-php
Charset: utf-8
<?php
if (!class_exists('UpdraftCentral_Remote_Communications')) :
/**
* This class dispatches command(s) to the remote website
*
* Pre-validates submitted data, sends command and processes
* the response received. Also responsible for caching result data
* whenever applicable, and can pull cached data whether or not having
* a maximum age (in seconds).
*/
class UpdraftCentral_Remote_Communications {
private $is_preencrypted;
private $command;
private $rc;
private $user;
private $data;
private $site_id;
private $site;
private $ud_rpc;
private $site_meta;
private $admin_url;
private $errors;
private $response;
private $table_prefix;
/**
* UpdraftCentral_Remote_Communications constructor.
*
* Does initial checks, pre-load certain properties needed to communicate to the remote website
* and loads an error collection needed when returning errors to the caller.
*
* @param object $user An UpdraftCentral_User instance
* @param array $response An initial response array
* @param array $post_data Request data for processing
* @return self
*/
public function __construct($user, $response, $post_data) {
if (!is_a($user, 'UpdraftCentral_User')) {
throw new Exception('Unexpected parameter. Argument to UpdraftCentral_Remote_Communications needs to be of type UpdraftCentral_User.');
}
$this->rc = UpdraftCentral();
$this->user = $user;
$this->data = $post_data;
$this->response = $response;
$this->is_preencrypted = !empty($post_data['site_rpc_preencrypted']);
$this->site_id = (int) $post_data['site_id'];
$this->site = $user->sites[$this->site_id];
$this->ud_rpc = $this->rc->get_udrpc($this->site->key_name_indicator);
$this->table_prefix = defined('UPDRAFTCENTRAL_TABLE_PREFIX') ? UPDRAFTCENTRAL_TABLE_PREFIX : 'updraftcentral_';
$this->site_meta = empty($user->sites_meta[$this->site->site_id]) ? array() : $user->sites_meta[$this->site->site_id];
$this->admin_url = empty($this->site->admin_url) ? $this->site->url : untrailingslashit($this->site->admin_url).'/admin-ajax.php';
if (preg_match('#/admin-ajax.php$#', $this->admin_url)) {
// wp-admin/admin-ajax.php before WP 3.5 will die() if $_REQUEST['action'] is not set (3.2) or is empty (3.4). Later WP versions also check that, but after (instead of before) wp-load.php, which is where we are ultimately hooked in.
$this->admin_url .= '?action=updraft_central';
}
$this->ud_rpc->set_destination_url($this->admin_url);
$this->load_errors();
}
/**
* Loads required objects if they're currently not available for the
* current request/process.
*
* @internal
*/
private function maybe_load_objects() {
// We'll make sure that we don't have an empty site_meta instance or
// any object needed by the process.
if (empty($this->rc)) {
$this->rc = UpdraftCentral();
}
if (empty($this->rc->site_meta)) {
if (!class_exists('UpdraftCentral_Site_Meta')) include_once UD_CENTRAL_DIR.'/classes/site-meta.php';
$this->rc->site_meta = new UpdraftCentral_Site_Meta($this->table_prefix);
}
}
/**
* Loads error collection that will be referenced when
* returning specific error code for a failed process.
*
* @internal
*/
private function load_errors() {
$errors = array(
'generic_error' => __('An error has occurred while processing your request.', 'updraftcentral'),
'missing_data' => __('Missing information', 'updraftcentral'),
'nonexistent_site' => sprintf(__('This site (%d / %d) was not found', 'updraftcentral'), $this->user->user_id, $this->site_id),
'nonexistent_site_key' => sprintf(__('The key for this site (%d / %d) was not found', 'updraftcentral'), $this->user->user_id, $this->site_id),
'site_unlicensed' => apply_filters('updraftcentral_site_unlicensed_message', __('You have more sites in your dashboard than licences.', 'updraftcentral').' '.__('As a result, you cannot perform actions on this site.', 'updraftcentral').' '.__('You will need to obtain more licences, or remove some sites.', 'updraftcentral')),
'cannot_contact_localdev' => __('You cannot contact a website hosted on a site-local network (e.g. localhost) from this dashboard - it cannot be reached.', 'updraftcentral'),
'no_digest_before_php54' => sprintf(__('To use HTTP digest authentication, your server running the UpdraftCentral dashboard needs at least PHP %s (your version is %s)', 'updraftcentral'), '5.4', PHP_VERSION),
'incompatible_udrpc_php' => sprintf(__('The loaded UDRPC library (%s) is too old - you probably need to update your installed UpdraftPlus on the server', 'updraftcentral'), $this->ud_rpc->version),
);
$this->errors = $errors;
}
/**
* Pulls information regarding the error
*
* @internal
* @param string $code A unique error code that is used to pull the error information
* @return array - Returns the error information for a specific error key/code
*/
private function return_error($code) {
$response = array(
'responsetype' => 'error',
'code' => $code,
'message' => isset($this->errors[$code]) ? $this->errors[$code] : $this->errors['generic_error']
);
return $response;
}
/**
* Validates if the cached data is still acceptable based from
* the maximum age required
*
* @internal
* @param string $created The time (number of seconds) when the data was cached/stored in DB
* @param int $maximum_age The maximum age (in seconds) to consider the cached data is still acceptable for consumption
* @return boolean - True if cached data is within the maximum age required, False otherwise
*/
private function check_data_validity($created, $maximum_age) {
return (time() - (int) $created) <= $maximum_age;
}
/**
* Cache the response received from the remote website
*
* @internal
* @param array $response The response array that contains the result of the command that was sent to the remote website
* @param string $meta_key A unique string that serves as an identifier for the cached data
* @return boolean - True if data was successfully cached, False otherwise
*/
private function cache_response($response, $meta_key) {
if (!empty($response)) {
// Check to see if we have an existing meta for data caching. If so, update the
// cached data with the current response, otherwise we'll create a new entry in DB.
$check = $this->rc->site_meta->get_site_meta($this->site_id, $meta_key, true);
if (!empty($check)) {
$result = $this->rc->site_meta->update_site_meta($this->site_id, $meta_key, $response);
} else {
$result = $this->rc->site_meta->add_site_meta($this->site_id, $meta_key, $response);
}
if (false !== $result) return true;
}
return false;
}
/**
* Pulls the cached/stored data either from the in-memory data loaded
* by the UpdraftCentral_User or from DB
*
* @internal
* @param string $meta_key A unique string that serves as an identifier for the cached data
* @param int $maximum_age The maximum age (in seconds) to consider the cached data is still acceptable for consumption
* @return mixed - Returns the stored data if successful, False otherwise
*/
private function get_cached_data($meta_key, $maximum_age) {
$maximum_age = empty($maximum_age) ? false : (int) $maximum_age;
if ($maximum_age) {
// Check in-memory data first (loaded under UpdraftCentral_User->load_user_sites()) before checking the DB
if (!empty($this->user->sites_meta[$this->site_id]) && isset($this->user->sites_meta[$this->site_id][$meta_key])) {
$loaded_data = $this->user->sites_meta[$this->site_id][$meta_key];
if ($this->check_data_validity($loaded_data->created, $maximum_age)) {
return $loaded_data->value;
}
}
// Anything can happen from the time the sites meta were loaded during UDC page load.
// Due to ajax requests it can be populated along the way, thus, if the above in-memory check
// fails we'll proceed in checking the DB.
$stored_data = $this->rc->site_meta->updraftcentral_get_site_metadata(null, $this->site_id, $meta_key, true, $maximum_age);
if (!empty($stored_data)) {
return $stored_data;
}
}
return false;
}
/**
* Cache response whenever applicable
*
* @internal
* @param string $command The command to execute
* @param array $response The response array that contains the result of the command that was sent to the remote website
* @param string $meta_key A unique string that serves as an identifier for the cached data
* @param boolean $force_save Optional. A flag to indicate whether we need to force the saving of the response from the remote website
* @return array - The original response array
*/
private function maybe_cache_response($command, $response, $meta_key, $force_save = false) {
// We're only saving the response to DB if $reply is not an instance
// of WP_Error class and command is currently not empty.
$reply = isset($response['reply']) ? $response['reply'] : $response;
if (!is_wp_error($reply) && !empty($command)) {
$remote_response = isset($reply['response']) ? $reply['response'] : null;
// Caching is only applicable to non-preencrypted data.
if (!$this->is_preencrypted && !empty($remote_response) && 'rpcerror' !== $remote_response) {
if ($force_save) {
// If $force_save is true then we're forced to save the response to DB. Most likely, this is
// set(required) from a CRON process where the results are force to be saved into DB.
$this->cache_response($response, $meta_key);
} else {
// Here, we're only storing/caching the response when needed, as not all commands
// requires caching. Add an 'updraftcentral_cache_commands' filter the UDC module if you wish to cache
// any specific commands.
$commands = apply_filters('updraftcentral_cache_commands', array());
if (in_array($command, $commands) && !empty($meta_key)) {
$this->cache_response($response, $meta_key);
}
}
}
}
return $response;
}
/**
* Do a post check of the result/response of the currently executed command
*
* @param array $result The result of the command that was sent to the remote website
* @return array - The request's response
*/
private function response_post_check($result) {
$caught_output = $result['caught_output'];
$reply = $result['reply'];
$response = $this->response;
// Pass on PHP events from the remote side
if (!empty($response['data']['php_events'])) $response['php_events'] = $response['data']['php_events'];
if (!empty($caught_output)) $response['mothership_caught_output'] = $caught_output;
if (is_wp_error($reply)) {
$response['responsetype'] = 'error';
$response['message'] = $reply->get_error_message();
$response['code'] = $reply->get_error_code();
$response['data'] = $reply->get_error_data();
} elseif (is_array($reply) && !empty($reply['response']) && 'error' == $reply['response']) {
$response['responsetype'] = 'error';
$response['message'] = empty($reply['message']) ? __('The connection to the remote site returned an error', 'updraftcentral') : $reply['message'];
$response['data'] = $reply;
} elseif ((!$this->is_preencrypted && (!is_array($reply) || empty($reply['response']) || (('ping' == $this->command && 'pong' != $reply['response'])) && 'rpcok' != $reply['response'])) || ($this->is_preencrypted && null === ($decoded_reply = json_decode($reply, true)) && (false == ($found_at = strpos($reply, '{"format":')) || null === ($decoded_reply = json_decode(substr($reply, $found_at), true))))) {
// If it is pre-encrypted, we expect a field 'udrpc_message' in the reply (after it's been JSON-decoded). We could check that. But instead, we just pass it back to the browser, since it'll be checked there anyway.
$response['responsetype'] = 'error';
$response['message'] = __('There was an error in contacting the remote site.', 'updraftcentral').' '.__("You should check that the remote site is online, is not firewalled, has remote control enabled, and that no security module is blocking the access.", 'updraftcentral').' '.__("Then, check the logs on the remote site and your browser's JavaScript console.", 'updraftcentral').' '.__('If none of that helps, then you should try re-adding the site with a fresh key.', 'updraftcentral');
$response['data'] = $reply;
$response['code'] = 'no_pong';
} else {
$response['responsetype'] = 'ok';
$response['message'] = __('The site was connected to, and returned a response', 'updraftcentral');
if ($this->is_preencrypted) {
$response['wrapped_response'] = $decoded_reply;
} elseif ('siteinfo' == $this->command) {
$response['rpc_response'] = $this->user->deep_sanitize($reply);
} else {
$response['rpc_response'] = $reply;
}
}
return $response;
}
/**
* Generates a unique key out from the site id, command and data parameters to
* be used as a meta key when saving the data to the DB.
*
* @param string $command The current command to execute
* @param array $data An array containing the command parameters
* @return string - The generated key
*/
public function generate_meta_key($command, $data) {
// The "maximum_age" info was purposely injected to aide in checking
// the freshness of data. Since this is not part of the original data parameter
// that is submitted alongside the command therefore, we remove it when generating a meta key.
if (isset($data['commands'])) {
// For multiplexed command, we run through each registered commands and remove the field
foreach ($data['commands'] as $key => $value) {
if (isset($value['maximum_age'])) unset($data['commands'][$key]['maximum_age']);
}
$data = $data['commands'];
} else {
if (isset($data['maximum_age'])) unset($data['maximum_age']);
}
return $this->user->generate_cache_key($this->site_id, $command, $data);
}
/**
* Retrieves a previously stored (cached) response from the remote site for the given command if
* available and that the maximum age of the data has not been reached or expired.
*
* @param string $command The current command to execute
* @param array $data An array containing the command parameters
* @return array
*/
private function retrieve_cached_data($command, $data) {
// Default: 10 minutes (600 seconds) if UPDRAFTCENTRAL_DATA_MAXIMUM_AGE is not defined
//
// N.B. The maximum_age should be found attached to the "data" parameter if the developer
// wishes to have a specific maximum age (freshness of data) for the current command, otherwise
// the default_maximum_age will be used.
$default_maximum_age = (defined('UPDRAFTCENTRAL_DATA_MAXIMUM_AGE')) ? UPDRAFTCENTRAL_DATA_MAXIMUM_AGE : 600;
// Generate a key for this command and its underlying data (command parameters) to be use for
// saving and retrieving the response to/from the DB.
$key = $this->generate_meta_key($command, $data);
$maximum_age = isset($data['maximum_age']) ? $data['maximum_age'] : $default_maximum_age;
// Retrieves and return the cached data using the "$key" as "meta_key" field in the "sites_meta" table if available
// and the maximum_age has not been reached.
$cached_data = $this->get_cached_data($key, $maximum_age);
$result = array();
if (!empty($cached_data)) {
$result = $this->response_post_check($cached_data);
}
// The generated key will serve as a reference for saving and retrieving the stored/cached value
// from the "sites_meta" table in DB. The key is composed of the currently requested or executed command
// (e.g. 'updates.get_updates') along with its submitted data that serves as parameters to the
// command (e.g. array('force_refresh' => false)).
return array(
'key' => $key,
'data' => $result
);
}
/**
* Sends command to the remote website and processes the response
*
* @param boolean $force_save Optional. A flag to indicate whether we need to force the saving of the response from the remote website
* @return array - The response array that contains the result of the currently processed command
*/
public function send_message($force_save = false) {
$this->command = isset($this->data['data']['command']) ? (string) $this->data['data']['command'] : '';
$is_multiplexed = ('core.execute_commands' === $this->command) ? true : false;
$data = isset($this->data['data']['data']) ? $this->data['data']['data'] : null;
if ($this->is_preencrypted) $data = $this->data['wrapped_message'];
$cached_data = array();
$computed_meta_key = '';
if (!empty($data)) {
// Possibly load required objects for this process if not available.
$this->maybe_load_objects();
// We're pulling individual cache data for the same sub-command
// if we've already had a previously cached response.
if ($is_multiplexed) {
$computed_keys = array();
$result = array();
// Make sure that we're getting the latest cached data for the command
// instead of an old result from a multiplexed command's response.
//
// N.B. Need to run through all available commands under the multiplexed
// command executed to get the latest (fresh) data that was previously cached
// if available.
foreach ($data['commands'] as $sub_command => $sub_data) {
$cached = $this->retrieve_cached_data($sub_command, $sub_data);
$computed_keys[$sub_command] = $cached['key'];
if (!empty($cached['data'])) $result[$sub_command] = $cached['data'];
}
// Update the reply with the latest cached response whenever applicable.
if (!empty($result)) {
$cached_data['data'] = array(
'reply' => $result
);
}
} else {
$cached_data = $this->retrieve_cached_data($this->command, $data);
$computed_meta_key = $cached_data['key'];
}
// Return any cached data found if not empty.
if (!empty($cached_data['data'])) {
// N.B. This is safe since we're only saving (caching) if we don't encounter any error in the caught_output
// as it won't make any sense if we're saving errors.
if (isset($cached_data['data']['reply']) && !isset($cached_data['data']['caught_output'])) {
$command_data = array(
'caught_output' => '',
'reply' => $cached_data['data']['reply']
);
$cached_data['data'] = $this->response_post_check($command_data);
}
return $cached_data['data'];
}
}
// If we reached this far then that would mean that we currently don't have any cache data
// associated with the submitted command. Thus, we will proceed in sending the request to the remote website.
// @codingStandardsIgnoreLine
@ob_start();
if (!empty($this->site_meta['http_username']->value)) {
$authentication_method = empty($this->site_meta['http_authentication_method']->value) ? 'basic' : $this->site_meta['http_authentication_method']->value;
$http_password = empty($this->site_meta['http_password']->value) ? '' : (string) $this->site_meta['http_password']->value;
if ('basic' != $authentication_method && version_compare(PHP_VERSION, '5.4', '<')) {
$error_code = 'no_digest_before_php54';
$reply = new WP_Error($error_code, $this->errors[$error_code], PHP_VERSION);
} else {
// Guzzle supports HTTP digest authentication - the WP HTTP API doesn't.
if (!class_exists('GuzzleHttp\Client')) include_once UD_CENTRAL_DIR.'/vendor/autoload.php';
$guzzle_client = new GuzzleHttp\Client();
if (!method_exists($this->ud_rpc, 'set_http_transport') || !method_exists($this->ud_rpc, 'set_http_credentials')) {
// That's the probable cause, because we can assume that UC has a bundled UDRPC that's new enough.
$error_code = 'incompatible_udrpc_php';
$reply = new WP_Error($error_code, $this->errors[$error_code]);
} else {
$this->ud_rpc->set_http_transport($guzzle_client);
$this->ud_rpc->set_http_credentials(array('username' => $this->site_meta['http_username']->value, 'password' => $http_password, 'authentication_method' => $authentication_method));
}
}
}
if ($this->is_preencrypted) {
// Command is not applicable to this area, so we empty it if we have a pre-encrypted data
// since subsequent process may want to check the command before proceeding.
$this->command = '';
$reply = $this->user->send_message($this->ud_rpc, '__updraftcentral_internal_preencrypted', $data, 30);
} else {
$this->ud_rpc->set_key_local($this->site->key_local_private);
$this->ud_rpc->set_key_remote($this->site->key_remote_public);
$this->ud_rpc->activate_replay_protection();
$reply = $this->user->send_message($this->ud_rpc, $this->command, $data, 30);
}
// @codingStandardsIgnoreStart
$caught_output = @ob_get_contents();
@ob_end_clean();
// @codingStandardsIgnoreEnd
// Check if we're currently running a multiplexed command. If so,
// we're going to save each individual results separately, so that it can be retrieved later
// when a command is sent individually with the same signature (command name and data parameters).
if ($is_multiplexed) {
// N.B. We don't cache the generic multiplexed command ('core.execute_commands') but instead
// we save each individual commands separately so that we can pull individual request's result
// that were cached/saved previously.
$output = array(
'caught_output' => $caught_output,
'reply' => $reply
);
$result = $this->response_post_check($output);
if ('ok' === $result['responsetype'] && is_array($result['rpc_response'])) {
// Save generic response signature here, we're going to use this later when
// caching individual responses for each executed commands by overriding its
// "data" field with the actual response data for that command before saving it to DB.
$store_data = $output;
foreach ($result['rpc_response']['data'] as $command => $data) {
if (isset($computed_keys[$command])) {
$key = $computed_keys[$command];
if ('rpcok' == $data['response']) {
$store_data['reply']['data'] = $data['data'];
$this->maybe_cache_response($command, $store_data, $key, $force_save);
}
}
}
}
} else {
// Make sure that we have a meta_key generated for the new request.
if (empty($computed_meta_key)) {
$computed_meta_key = $this->generate_meta_key($this->command, $data);
}
// Cache response whenever applicable
$result = $this->response_post_check($this->maybe_cache_response($this->command, array(
'caught_output' => $caught_output,
'reply' => $reply
), $computed_meta_key, $force_save));
}
return $result;
}
/**
* Validates the submitted data before sending the command to the remote site
*
* @return mixed - True on success, an error array containing the error information on failure
*/
public function validate_input() {
if (!empty($this->data)) {
// Check the sent data
if ($this->is_preencrypted) {
if (!isset($this->data['wrapped_message']) || !is_array($this->data['wrapped_message']) || empty($this->data['site_id']) || !is_numeric($this->data['site_id'])) {
return $this->return_error('missing_data');
}
} else {
if (!isset($this->data['data']) || !is_array($this->data['data']) || empty($this->data['data']['command']) || empty($this->data['site_id']) || !is_numeric($this->data['site_id'])) {
return $this->return_error('missing_data');
}
}
// This is also a security check - whether the specified site belongs to the current user
if (empty($this->site)) {
return $this->return_error('nonexistent_site');
}
if (empty($this->site->key_local_private)) {
return $this->return_error('nonexistent_site_key');
}
if (!empty($this->site->unlicensed)) {
return $this->return_error('site_unlicensed');
}
if ($this->rc->url_looks_internal($this->site->url) && !$this->rc->url_looks_internal(site_url()) && !apply_filters('updraftcentral_allow_contacting_internal_url_from_server', true, $this->site->url)) {
return $this->return_error('cannot_contact_localdev');
}
return true;
} else {
return $this->return_error('missing_data');
}
}
}
endif;