first commit

This commit is contained in:
Roman Pyrih
2026-04-21 15:48:41 +02:00
commit 7483681901
10216 changed files with 3236626 additions and 0 deletions

View File

@@ -0,0 +1,440 @@
<?php
if (!defined('UPDRAFTCENTRAL_CLIENT_DIR')) die('No access.');
/**
* Handles Analytics Commands
*
* @method array ga_checker()
* @method array get_access_token()
* @method array set_authorization_code()
*/
class UpdraftCentral_Analytics_Commands extends UpdraftCentral_Commands {
private $scope = 'https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/analytics.readonly';
private $endpoint = 'https://accounts.google.com/o/oauth2/auth';
private $token_info_endpoint = 'https://www.googleapis.com/oauth2/v1/tokeninfo';
private $access_key = 'updraftcentral_auth_server_access';
private $auth_endpoint;
private $client_id;
private $view_key = 'updraftcentral_analytics_views';
private $tracking_id_key = 'updraftcentral_analytics_tracking_id';
private $expiration;
/**
* Constructor
*/
public function __construct() {
$this->auth_endpoint = defined('UPDRAFTPLUS_GOOGLE_ANALYTICS_CALLBACK_URL') ? UPDRAFTPLUS_GOOGLE_ANALYTICS_CALLBACK_URL : 'https://auth.updraftplus.com/auth/googleanalytics';
$this->client_id = defined('UPDRAFTPLUS_GOOGLE_ANALYTICS_CLIENT_ID') ? UPDRAFTPLUS_GOOGLE_ANALYTICS_CLIENT_ID : '306245874349-6s896c3tjpra26ns3dpplhqcl6rv6qlb.apps.googleusercontent.com';
// Set transient expiration - default for 24 hours
$this->expiration = 86400;
}
/**
* Checks whether Google Analytics (GA) is installed or setup
*
* N.B. This check assumes GA is installed either using "wp_head" or "wp_footer" (e.g. attached
* to the <head/> or somewhere before </body>). It does not recursively check all the pages
* of the website to find if GA is installed on each or one of those pages, but only on the main/root page.
*
* @return array $result An array containing "ga_installed" property which returns "true" if GA (Google Analytics) is installed, "false" otherwise.
*/
public function ga_checker() {
try {
// Retrieves the tracking code/id if available
$tracking_id = $this->get_tracking_id();
$installed = true;
// If tracking code/id is currently not available then we
// parse the needed information from the buffered content through
// the "wp_head" and "wp_footer" hooks.
if (false === $tracking_id) {
$info = $this->extract_tracking_id();
$installed = $info['installed'];
$tracking_id = $info['tracking_id'];
}
// Get access token to be use to generate the report.
$access_token = $this->_get_token();
if (empty($access_token)) {
// If we don't get a valid access token then that would mean
// the access has been revoked by the user or UpdraftCentral was not authorized yet
// to access the user's analytics data, thus, we're clearing
// any previously stored user access so we're doing some housekeeping here.
$this->clear_user_access();
}
// Wrap and combined information for the requesting
// client's consumption
$result = array(
'ga_installed' => $installed,
'tracking_id' => $tracking_id,
'client_id' => $this->client_id,
'redirect_uri' => $this->auth_endpoint,
'scope' => $this->scope,
'access_token' => $access_token,
'endpoint' => $this->endpoint
);
} catch (Exception $e) {
$result = array('error' => true, 'message' => 'generic_response_error', 'values' => array($e->getMessage()));
}
return $this->_response($result);
}
/**
* Extracts Google Tracking ID from contents rendered through the "wp_head" and "wp_footer" action hooks
*
* @internal
* @return array $result An array containing the result of the extraction.
*/
private function extract_tracking_id() {
// Define result array
$result = array();
// Retrieve header content
ob_start();
do_action('wp_head');
$header_content = ob_get_clean();
// Extract analytics information if available.
$output = $this->parse_content($header_content);
$result['installed'] = $output['installed'];
$result['tracking_id'] = $output['tracking_id'];
// If it was not found, then now try the footer
if (empty($result['tracking_id'])) {
// Retrieve footer content
ob_start();
do_action('wp_footer');
$footer_content = ob_get_clean();
$output = $this->parse_content($footer_content);
$result['installed'] = $output['installed'];
$result['tracking_id'] = $output['tracking_id'];
}
if (!empty($result['tracking_id'])) {
set_transient($this->tracking_id_key, $result['tracking_id'], $this->expiration);
}
return $result;
}
/**
* Gets access token
*
* Validates whether the system currently have a valid token to use when connecting to Google Analytics API.
* If not, then it will send a token request based on the authorization code we stored during the
* authorization phase. Otherwise, it will return an empty token.
*
* @return array $result An array containing the Google Analytics API access token.
*/
public function get_access_token() {
try {
// Loads or request a valid token to use
$access_token = $this->_get_token();
if (!empty($access_token)) {
$result = array('access_token' => $access_token);
} else {
$result = array('error' => true, 'message' => 'ga_token_retrieval_failed', 'values' => array());
}
} catch (Exception $e) {
$result = array('error' => true, 'message' => 'generic_response_error', 'values' => array($e->getMessage()));
}
return $this->_response($result);
}
/**
* Clears any previously stored user access
*
* @return bool
*/
public function clear_user_access() {
return delete_option($this->access_key);
}
/**
* Saves user is and access token received from the auth server
*
* @param array $query Parameter array containing the user id and access token from the auth server.
* @return array $result An array containing a "success" or "failure" message as a result of the current process.
*/
public function save_user_access($query) {
try {
$token = get_option($this->access_key, false);
$result = array();
if (false === $token) {
$token = array(
'user_id' => base64_decode(urldecode($query['user_id'])),
'access_token' => base64_decode(urldecode($query['access_token']))
);
if (false !== update_option($this->access_key, $token)) {
$result = array('error' => false, 'message' => 'ga_access_saved', 'values' => array());
} else {
$result = array('error' => true, 'message' => 'ga_access_saving_failed', 'values' => array($query['access_token']));
}
}
} catch (Exception $e) {
$result = array('error' => true, 'message' => 'generic_response_error', 'values' => array($e->getMessage()));
}
return $this->_response($result);
}
/**
* Saves the tracking code/id manually (user input)
*
* @param array $query Parameter array containing the tracking code/id to save.
* @return array $result An array containing the result of the process.
*/
public function save_tracking_id($query) {
try {
$tracking_id = $query['tracking_id'];
$saved = false;
if (!empty($tracking_id)) {
$saved = set_transient($this->tracking_id_key, $tracking_id, $this->expiration);
}
$result = array('saved' => $saved);
} catch (Exception $e) {
$result = array('error' => true, 'message' => 'generic_response_error', 'values' => array($e->getMessage()));
}
return $this->_response($result);
}
/**
* Retrieves any available access token either previously saved info or
* from a new request from the Google Server.
*
* @internal
* @return string $authorization_code
*/
private function _get_token() {
// Retrieves the tracking code/id if available
$tracking_id = $this->get_tracking_id();
$access_token = '';
$token = get_option($this->access_key, false);
if (false !== $token) {
$access_token = isset($token['access_token']) ? $token['access_token'] : '';
$user_id = isset($token['user_id']) ? $token['user_id'] : '';
if ((!empty($access_token) && !$this->_token_valid($access_token)) || (!empty($user_id) && empty($access_token) && !empty($tracking_id))) {
if (!empty($user_id)) {
$args = array(
'headers' => apply_filters('updraftplus_auth_headers', array())
);
$response = wp_remote_get($this->auth_endpoint.'?user_id='.$user_id.'&code=ud_googleanalytics_code', $args);
if (is_wp_error($response)) {
throw new Exception($response->get_error_message()); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- The escaping should be happening when the exception is printed
} else {
if (is_array($response)) {
$body = json_decode($response['body'], true);
$token_response = array();
if (is_array($body) && !isset($body['error'])) {
$token_response = json_decode(base64_decode($body[0]), true);
}
if (is_array($token_response) && isset($token_response['access_token'])) {
$access_token = $token_response['access_token'];
} else {
// If we don't get any valid response then that would mean that the
// permission was already revoked. Thus, we need to re-authorize the
// user before using the analytics feature once again.
$access_token = '';
}
$token['access_token'] = $access_token;
update_option($this->access_key, $token);
}
}
}
}
}
return $access_token;
}
/**
* Verifies whether the access token is still valid for use
*
* @internal
* @param string $token The access token to be check and validated
* @return bool
* @throws Exception If an error has occurred while connecting to the Google Server.
*/
private function _token_valid($token) {
$response = wp_remote_get($this->token_info_endpoint.'?access_token='.$token);
if (is_wp_error($response)) {
throw new Exception($response->get_error_message()); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- The escaping should be happening when the exception is printed
} else {
if (is_array($response)) {
$response = json_decode($response['body'], true);
if (!empty($response)) {
if (!isset($response['error']) && !isset($response['error_description'])) {
return true;
}
}
}
}
return false;
}
/**
* Parses and extracts the google analytics information (NEEDED)
*
* @internal
* @param string $content The content to parse
* @return array An array containing the status of the process along with the tracking code/id
*/
private function parse_content($content) {
$installed = false;
$gtm_installed = false;
$tracking_id = '';
$script_file_found = false;
$tracking_id_found = false;
// Pull google analytics script file(s)
preg_match_all('/<script\b[^>]*>([\s\S]*?)<\/script>/i', $content, $scripts);
for ($i=0; $i < count($scripts[0]); $i++) {
// Check for Google Analytics file
if (stristr($scripts[0][$i], 'ga.js') || stristr($scripts[0][$i], 'analytics.js')) {
$script_file_found = true;
}
// Check for Google Tag Manager file
// N.B. We are not checking for GTM but this check will be useful when
// showing the notice to the user if we haven't found Google Analytics
// directly being installed on the page.
if (stristr($scripts[0][$i], 'gtm.js')) {
$gtm_installed = true;
}
}
// Pull tracking code
preg_match_all('/UA-[0-9]{5,}-[0-9]{1,}/i', $content, $codes);
if (count($codes) > 0) {
if (!empty($codes[0])) {
$tracking_id_found = true;
$tracking_id = $codes[0][0];
}
}
// If we found both the script and the tracking code then it is safe
// to say that Google Analytics (GA) is installed. Thus, we're returning
// "true" as a response.
if ($script_file_found && $tracking_id_found) {
$installed = true;
}
// Return result of process.
return array(
'installed' => $installed,
'gtm_installed' => $gtm_installed,
'tracking_id' => $tracking_id
);
}
/**
* Retrieves the "analytics_tracking_id" transient
*
* @internal
* @return mixed Returns the value of the saved transient. Returns "false" if the transient does not exist.
*/
private function get_tracking_id() {
return get_transient($this->tracking_id_key);
}
/**
* Returns the current tracking id
*
* @return array $result An array containing the Google Tracking ID.
*/
public function get_current_tracking_id() {
try {
// Get current site transient stored for this key
$tracking_id = get_transient($this->tracking_id_key);
// Checks whether we have a valid token
$access_token = $this->_get_token();
if (empty($access_token)) {
$tracking_id = '';
}
if (false === $tracking_id) {
$result = $this->extract_tracking_id();
} else {
$result = array(
'installed' => true,
'tracking_id' => $tracking_id
);
}
} catch (Exception $e) {
$result = array('error' => true, 'message' => 'generic_response_error', 'values' => array($e->getMessage()));
}
return $this->_response($result);
}
/**
* Clears user access from database
*
* @return array $result An array containing the "Remove" confirmation whether the action succeeded or not.
*/
public function remove_user_access() {
try {
// Clear user access
$is_cleared = $this->clear_user_access();
if (false !== $is_cleared) {
$result = array('removed' => true);
} else {
$result = array('error' => true, 'message' => 'user_access_remove_failed', 'values' => array());
}
} catch (Exception $e) {
$result = array('error' => true, 'message' => 'generic_response_error', 'values' => array($e->getMessage()));
}
return $this->_response($result);
}
}

View File

@@ -0,0 +1,391 @@
<?php
if (!defined('UPDRAFTCENTRAL_CLIENT_DIR')) die('No access.');
/**
* Handles Backups Commands
*/
class UpdraftCentral_Backups_Commands extends UpdraftCentral_Commands {
private $switched = false;
/**
* Function that gets called before every action
*
* @param string $command a string that corresponds to UDC command to call a certain method for this class.
* @param array $data an array of data post or get fields
* @param array $extra_info extrainfo use in the udrpc_action, e.g. user_id
*
* link to udrpc_action main function in class UpdraftCentral_Listener
*/
public function _pre_action($command, $data, $extra_info) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- This function is called from listener.php and $extra_info is being sent.
// Here we assign the current blog_id to a variable $blog_id
$blog_id = get_current_blog_id();
if (!empty($data['site_id'])) $blog_id = $data['site_id'];
if (function_exists('switch_to_blog') && is_multisite() && $blog_id) {
$this->switched = switch_to_blog($blog_id);
}
}
/**
* Function that gets called after every action
*
* @param string $command a string that corresponds to UDC command to call a certain method for this class.
* @param array $data an array of data post or get fields
* @param array $extra_info extrainfo use in the udrpc_action, e.g. user_id
*
* link to udrpc_action main function in class UpdraftCentral_Listener
*/
public function _post_action($command, $data, $extra_info) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Unused parameter is present because the caller from UpdraftCentral_Listener class uses 3 arguments.
// Here, we're restoring to the current (default) blog before we switched
if ($this->switched) restore_current_blog();
}
/**
* Retrieves the UpdraftPlus plugin status, UpdraftVault storage usage status, Next backup
* schedule, etc. Used primarily by UpdraftCentral background process.
*
* @return array
*/
public function get_status() {
if (!current_user_can('manage_options')) {
$response = array(
'status' => 'error',
'error_code' => 'insufficient_permission',
);
} else {
if (!function_exists('get_mu_plugins')) include_once(ABSPATH.'wp-admin/includes/plugin.php');
$mu_plugins = get_mu_plugins();
$is_premium = false;
if (defined('UPDRAFTPLUS_DIR') && file_exists(UPDRAFTPLUS_DIR.'/udaddons')) $is_premium = true;
// Set default response
$response = array(
'updraftplus_version' => '',
'is_premium' => $is_premium,
'installed' => false,
'active' => false,
'backup_count' => 0,
'has_mu_plugins' => !empty($mu_plugins) ? true : false,
'last_backup' => array(
'backup_nonce' => '',
'has_errors' => false,
'has_warnings' => false,
'has_succeeded' => false,
),
'updraftvault' => array(
'site_connected' => false,
'storage' => array('quota_used' => '0 MB', 'quota' => '0 MB', 'percentage_usage' => '0.0%'),
),
'meta' => array(),
);
if (class_exists('UpdraftPlus')) {
global $updraftplus;
$response['updraftplus_version'] = $updraftplus->version;
$response['updraftvault'] = $this->get_updraftvault_status();
$response['installed'] = true;
$response['active'] = true;
$response['meta'] = $this->get_filesystem_credentials_info();
$schedule = $this->get_next_backup_schedule();
if ($schedule) {
$response['next_backup_schedule'] = $schedule;
}
$backup_history = UpdraftPlus_Backup_History::add_jobdata(UpdraftPlus_Backup_History::get_history());
$response['backup_count'] = count($backup_history);
$updraft_last_backup = UpdraftPlus_Options::get_updraft_option('updraft_last_backup');
if ($updraft_last_backup) {
$response['last_backup']['backup_nonce'] = $updraft_last_backup['backup_nonce'];
if (isset($updraft_last_backup['backup_time'])) {
$response['last_backup']['backup_date'] = gmdate('n/j/Y', $updraft_last_backup['backup_time']);
$response['last_backup']['backup_time'] = $updraft_last_backup['backup_time'];
}
$errors = 0;
$warnings = 0;
if (is_array($updraft_last_backup['errors'])) {
foreach ($updraft_last_backup['errors'] as $err) {
$level = (is_array($err)) ? $err['level'] : 'error';
if ('warning' == $level) {
$warnings++;
} elseif ('error' == $level) {
$errors++;
}
}
}
if ($errors > 0) $response['last_backup']['has_errors'] = true;
if ($warnings > 0) $response['last_backup']['has_warnings'] = true;
if (isset($updraft_last_backup['success']) && $updraft_last_backup['success']) $response['last_backup']['has_succeeded'] = true;
}
} else {
if (!function_exists('get_plugins')) require_once(ABSPATH.'wp-admin/includes/plugin.php');
$plugins = get_plugins();
$key = 'updraftplus/updraftplus.php';
if (array_key_exists($key, $plugins)) {
$response['installed'] = true;
if (is_plugin_active($key)) $response['active'] = true;
}
}
}
return $this->_response($response);
}
/**
* Retrieves the next backup schedule for Files and Database backups
*
* @return string
*/
private function get_next_backup_schedule() {
// Get the next (nearest) scheduled backups
$files = wp_next_scheduled('updraft_backup');
$db = wp_next_scheduled('updraft_backup_database');
if ($files && $db) {
$timestamp = min($files, $db); // Get the nearest schedule among the two schedules
} elseif ($files && !$db) {
$timestamp = $files;
} elseif (!$files && $db) {
$timestamp = $db;
} else {
$timestamp = null;
}
if (!empty($timestamp)) {
return gmdate('g:i A - D', $timestamp);
}
return false;
}
/**
* Retrieves the UpdrafVault storage usage status
*
* @return array
*/
private function get_updraftvault_status() {
if (!class_exists('UpdraftCentral_UpdraftVault_Commands')) {
include_once(UPDRAFTPLUS_DIR.'/includes/updraftvault.php');
}
$updraftvault = new UpdraftCentral_UpdraftVault_Commands($this->rc);
$creds = $updraftvault->get_credentials();
$site_connected = false;
$storage = array('quota_used' => '0 MB', 'quota' => '0 MB', 'percentage_usage' => '0.0%');
$remote_service = false;
if (isset($creds['data'])) {
if (!isset($creds['data']['error']) && isset($creds['data']['accesskey'])) {
$site_connected = true;
$storage_objects_and_ids = UpdraftPlus_Storage_Methods_Interface::get_storage_objects_and_ids(array('updraftvault'));
if (isset($storage_objects_and_ids['updraftvault']['instance_settings'])) {
$instance_settings = $storage_objects_and_ids['updraftvault']['instance_settings'];
$instance_id = key($instance_settings);
$opts = $instance_settings[$instance_id];
if (!class_exists('UpdraftPlus_BackupModule_updraftvault')) {
include_once(UPDRAFTPLUS_DIR.'/methods/updraftvault.php');
}
$vault = new UpdraftPlus_BackupModule_updraftvault();
$vault->set_options($opts, false, $instance_id);
$quota_root = $opts['quota_root'];
$quota = $opts['quota'];
if (empty($quota_root)) {
// This next line is wrong: it lists the files *in this site's sub-folder*, rather than the whole Vault
$current_files = $vault->listfiles('');
} else {
$current_files = $vault->listfiles_with_path($quota_root, '', true);
}
if (!is_wp_error($current_files) && is_array($current_files)) {
$quota_used = 0;
foreach ($current_files as $file) {
$quota_used += $file['size'];
}
$storage = array(
'quota_used' => round($quota_used / 1048576, 1).' MB',
'quota' => round($quota / 1048576, 1).' MB',
'percentage_usage' => sprintf('%.1f', 100*$quota_used / $quota).'%',
);
$remote_service = array(
'name' => 'updraft_include_remote_service_updraftvault',
'value' => $instance_id,
);
}
}
}
}
return array(
'site_connected' => $site_connected,
'storage' => $storage,
'remote_service' => $remote_service,
);
}
/**
* Retrieves information whether filesystem credentials (e.g. FTP/SSH) are required
* when updating plugins
*
* @return array
*/
private function get_filesystem_credentials_info() {
if (!function_exists('get_filesystem_method')) {
include_once(ABSPATH.'/wp-admin/includes/file.php');
}
$filesystem_method = get_filesystem_method(array(), WP_PLUGIN_DIR);
ob_start();
$filesystem_credentials_are_stored = request_filesystem_credentials(site_url());
$filesystem_form = strip_tags(ob_get_contents(), '<div><h2><p><input><label><fieldset><legend><span><em>');
ob_end_clean();
$request_filesystem_credentials = ('direct' != $filesystem_method && !$filesystem_credentials_are_stored);
return array(
'request_filesystem_credentials' => $request_filesystem_credentials,
'filesystem_form' => base64_encode($filesystem_form),
);
}
/**
* Retrieves the backup progress in terms of entities completed. Used primarily by UpdraftCentral
* for polling backup progress in the background.
*
* @param array $params Submitted arguments for the current request
* @return array
*/
public function get_backup_progress($params) {
$nonce = isset($params['nonce']) ? $params['nonce'] : false;
$response = array('nonce' => $params['nonce']);
if (!current_user_can('manage_options')) {
$response['status'] = 'error';
$response['error_code'] = 'insufficient_permission';
} else {
global $updraftplus;
if ($nonce && $updraftplus && is_a($updraftplus, 'UpdraftPlus')) {
// Check the job is not still running.
$jobdata = $updraftplus->jobdata_getarray($nonce);
if (!empty($jobdata)) {
$response['status'] = 'in-progress';
$file_entities = 0;
$db_entities = 0;
$processed = 0;
if (isset($jobdata['backup_database']) && 'no' != $jobdata['backup_database']) {
$backup_database = $jobdata['backup_database'];
$db_entities += count($backup_database);
foreach ($backup_database as $whichdb => $info) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- In this check we only need the status contained in the $info for now.
$status = $info; // For default: 'wp'
if (is_array($info)) {
$status = $info['status'];
}
if ('finished' == $status) {
$processed++;
}
}
}
if (isset($jobdata['backup_files']) && 'no' != $jobdata['backup_files']) {
$file_entities = count($jobdata['job_file_entities']);
$backup_files = $jobdata['backup_files'];
if ('finished' == $backup_files) {
$processed += $file_entities;
} elseif (isset($jobdata['filecreating_substatus'])) {
$substatus = $jobdata['filecreating_substatus'];
$processed += max(0, intval($substatus['i']) - 1);
}
}
$response['progress'] = array(
'file_entities' => $file_entities,
'db_entities' => $db_entities,
'total_entities' => $file_entities+$db_entities,
'processed' => $processed,
'percentage' => floor(($processed/($file_entities+$db_entities))*100),
'nonce' => $nonce,
);
UpdraftPlus_Options::update_updraft_option('updraft_central_last_backup_progress', $response['progress'], false);
} else {
$last_backup = UpdraftPlus_Options::get_updraft_option('updraft_last_backup');
if ($nonce == $last_backup['backup_nonce']) {
$response['status'] = 'finished';
$response['progress'] = array('percentage' => 100);
$response['progress']['errors'] = $last_backup['errors'];
$response['progress']['backup_time'] = $last_backup['backup_time'];
$response['progress']['completed_time'] = gmdate('g:ia', $last_backup['backup_time']);
$response['progress']['completed_date'] = gmdate('M d, Y', $last_backup['backup_time']);
$errors = 0;
$warnings = 0;
if (!empty($last_backup['errors']) && is_array($last_backup['errors'])) {
foreach ($last_backup['errors'] as $err) {
$level = (is_array($err)) ? $err['level'] : 'error';
if ('warning' == $level) {
$warnings++;
} elseif ('error' == $level) {
$errors++;
}
}
}
$response['progress']['has_errors'] = ($errors > 0) ? true : false;
$response['progress']['has_warnings'] = ($warnings > 0) ? true : false;
} else {
// We might be too early to check the `updraft_last_backup` thus, we'll
// give it a few rounds to check by setting the status to "in-progress"
// and returning the last backup progress (if applicable).
$last_progress = UpdraftPlus_Options::get_updraft_option('updraft_central_last_backup_progress');
$response['status'] = 'in-progress';
if (!empty($last_progress) && isset($last_progress['nonce'])) {
$response['progress'] = $last_progress;
if ($nonce == $last_progress['nonce']) {
UpdraftPlus_Options::delete_updraft_option('updraft_central_last_backup_progress');
}
}
}
}
}
}
return $this->_response($response);
}
}

View File

@@ -0,0 +1,842 @@
<?php
if (!defined('UPDRAFTCENTRAL_CLIENT_DIR')) die('No access.');
class UpdraftCentral_Comments_Commands extends UpdraftCentral_Commands {
/**
* The _search_comments function searches all available comments based
* on the following query parameters (type, status, search)
*
* Search Parameters/Filters:
* type - comment types can be 'comment', 'trackback' and 'pingback', defaults to 'comment'
* status - comment status can be 'hold' or unapprove, 'approve', 'spam', 'trash'
* search - user generated content or keyword
*
* @param array $query The query to search comments
* @return array
*/
private function _search_comments($query) {
// Basic parameters to the query and should display
// the results in descending order (latest comments) first
// based on their generated IDs
$args = array(
'orderby' => 'ID',
'order' => 'DESC',
'type' => $query['type'],
'status' => $query['status'],
'search' => esc_attr($query['search']),
);
$query = new WP_Comment_Query;
$found_comments = $query->query($args);
$comments = array();
foreach ($found_comments as $comment) {
// We're returning a collection of comment in an array,
// in sync with the originator of the request on the ui side
// so, we're pulling it one by one into the array before
// returning it.
if (!in_array($comment, $comments)) {
array_push($comments, $comment);
}
}
return $comments;
}
/**
* The _calculate_pages function generates and builds the pagination links
* based on the current search parameters/filters. Please see _search_comments
* for the breakdown of these parameters.
*
* @param array $query Query to generate pagination links
* @return array
*/
private function _calculate_pages($query) {
$per_page_options = array(10, 20, 30, 40, 50);
if (!empty($query)) {
if (!empty($query['search'])) {
return array(
'page_count' => 1,
'page_no' => 1
);
}
$pages = array();
$page_query = new WP_Comment_Query;
// Here, we're pulling the comments based on the
// two parameters namely type and status.
//
// The number of results/comments found will then
// be use to compute for the number of pages to be
// displayed as navigation links when browsing all
// comments from the frontend.
$comments = $page_query->query(array(
'type' => $query['type'],
'status' => $query['status']
));
$total_comments = count($comments);
$page_count = ceil($total_comments / $query['per_page']);
if ($page_count > 1) {
for ($i = 0; $i < $page_count; $i++) {
if ($i + 1 == $query['page_no']) {
$paginator_item = array(
'value' => $i+1,
'setting' => 'disabled'
);
} else {
$paginator_item = array(
'value' => $i+1
);
}
array_push($pages, $paginator_item);
}
if ($query['page_no'] >= $page_count) {
$page_next = array(
'value' => $page_count,
'setting' => 'disabled'
);
} else {
$page_next = array(
'value' => $query['page_no'] + 1
);
}
if (1 === $query['page_no']) {
$page_prev = array(
'value' => 1,
'setting' => 'disabled'
);
} else {
$page_prev = array(
'value' => $query['page_no'] - 1
);
}
return array(
'page_no' => $query['page_no'],
'per_page' => $query['per_page'],
'page_count' => $page_count,
'pages' => $pages,
'page_next' => $page_next,
'page_prev' => $page_prev,
'total_results' => $total_comments,
'per_page_options' => $per_page_options
);
} else {
return array(
'page_no' => $query['page_no'],
'per_page' => $query['per_page'],
'page_count' => $page_count,
'total_results' => $total_comments,
'per_page_options' => $per_page_options
);
}
} else {
return array(
'per_page_options' => $per_page_options
);
}
}
/**
* The get_blog_sites function pulls blog sites available for the current WP instance.
* If Multisite is enabled on the server, then sites under the network will be pulled, otherwise, it will return an empty array.
*
* @return array
*/
private function get_blog_sites() {
if (!is_multisite()) return array();
// Initialize array container
$sites = $network_sites = array();
// Check to see if latest get_sites (available on WP version >= 4.6) function is
// available to pull any available sites from the current WP instance. If not, then
// we're going to use the fallback function wp_get_sites (for older version).
if (function_exists('get_sites') && class_exists('WP_Site_Query')) {
$network_sites = get_sites();
} else {
if (function_exists('wp_get_sites')) {
$network_sites = wp_get_sites();
}
}
// We only process if sites array is not empty, otherwise, bypass
// the next block.
if (!empty($network_sites)) {
foreach ($network_sites as $site) {
// Here we're checking if the site type is an array, because
// we're pulling the blog_id property based on the type of
// site returned.
// get_sites returns an array of object, whereas the wp_get_sites
// function returns an array of array.
$blog_id = (is_array($site)) ? $site['blog_id'] : $site->blog_id;
// We're saving the blog_id and blog name as an associative item
// into the sites array, that will be used as "Sites" option in
// the frontend.
$sites[$blog_id] = get_blog_details($blog_id)->blogname;
}
}
return $sites;
}
/**
* The get_wp_option function pulls current blog options
* from the database using either following functions:
* - get_blog_option (for multisite)
* - get_option (for ordinary blog)
*
* @param array $blog_id This is the specific blog ID
* @param array $setting specifies settings
* @return array
*/
private function _get_wp_option($blog_id, $setting) {
return is_multisite() ? get_blog_option($blog_id, $setting) : get_option($setting);
}
/**
* The get_comments function pull all the comments from the database
* based on the current search parameters/filters. Please see _search_comments
* for the breakdown of these parameters.
*
* @param array $query Specific query to pull comments
* @return array
*/
public function get_comments($query) {
// Here, we're getting the current blog id. If blog id
// is passed along with the parameters then we override
// that current (default) value with the parameter blog id value.
$blog_id = get_current_blog_id();
if (isset($query['blog_id'])) $blog_id = $query['blog_id'];
// Here, we're switching to the actual blog that we need
// to pull comments from.
$switched = false;
if (function_exists('switch_to_blog')) {
$switched = switch_to_blog($blog_id);
}
if (!empty($query['search'])) {
// If a search keyword is present, then we'll call the _search_comments
// function to process the query.
$comments = $this->_search_comments($query);
} else {
// Set default parameter values if the designated
// parameters are empty.
if (empty($query['per_page'])) {
$query['per_page'] = 10;
}
if (empty($query['page_no'])) {
$query['page_no'] = 1;
}
if (empty($query['type'])) {
$query['type'] = '';
}
if (empty($query['status'])) {
$query['status'] = '';
}
// Since WP_Comment_Query parameters doesn't have a "page" attribute, we
// need to compute for the offset to get the exact content based on the
// current page and the number of items per page.
$offset = ((int) $query['page_no'] - 1) * (int) $query['per_page'];
$args = array(
'orderby' => 'ID',
'order' => 'DESC',
'number' => $query['per_page'],
'offset' => $offset,
'type' => $query['type'],
'status' => $query['status']
);
$comments_query = new WP_Comment_Query;
$comments = $comments_query->query($args);
}
// If no comments are found based on the current query then
// we return with error.
if (empty($comments)) {
$result = array('message' => 'comments_not_found');
return $this->_response($result);
}
// Otherwise, we're going to process each comment
// before we return it to the one issuing the request.
//
// Process in the sense that we add additional related info
// such as the post tile where the comment belongs to, the
// comment status, a formatted date field, and to which parent comment
// does the comment was intended to be as a reply.
foreach ($comments as &$comment) {
$comment = get_comment($comment->comment_ID, ARRAY_A);
if ($comment) {
$post = get_post($comment['comment_post_ID']);
if ($post) $comment['in_response_to'] = $post->post_title;
if (!empty($comment['comment_parent'])) {
$parent_comment = get_comment($comment['comment_parent'], ARRAY_A);
if ($parent_comment) $comment['in_reply_to'] = $parent_comment['comment_author'];
}
// We're formatting the comment_date to be exactly the same
// with that of WP Comments table (e.g. 2016/12/21 at 10:30 PM)
$comment['comment_date'] = date('Y/m/d \a\t g:i a', strtotime($comment['comment_date']));
$status = wp_get_comment_status($comment['comment_ID']);
if ($status) {
$comment['comment_status'] = $status;
}
}
}
// We return the following to the one issuing
// the request.
$result = array(
'comments' => $comments,
'paging' => $this->_calculate_pages($query)
);
// Here, we're restoring to the current (default) blog before we
// do the switched.
if (function_exists('restore_current_blog') && $switched) {
restore_current_blog();
}
return $this->_response($result);
}
/**
* The get_comment_filters function builds a array of options
* to be use as filters for the search function on the frontend.
*/
public function get_comment_filters() {
// Options for comment_types field
$comment_types = apply_filters('admin_comment_types_dropdown', array(
'comment' => __('Comments'),// phpcs:ignore WordPress.WP.I18n.MissingArgDomain -- The string exists within the WordPress core.
'pings' => __('Pings'),// phpcs:ignore WordPress.WP.I18n.MissingArgDomain -- The string exists within the WordPress core.
));
// Options for comment_status field
$comment_statuses = array(
'approve' => __('Approve'),// phpcs:ignore WordPress.WP.I18n.MissingArgDomain -- The string exists within the WordPress core.
'hold' => __('Hold or Unapprove', 'updraftplus'),
'trash' => __('Trash', 'updraftplus'),
'spam' => __('Spam', 'updraftplus'),
);
// Pull sites options if available.
$sites = $this->get_blog_sites();
$result = array(
'sites' => $sites,
'types' => $comment_types,
'statuses' => $comment_statuses,
'paging' => $this->_calculate_pages(null),
);
return $this->_response($result);
}
/**
* The get_settings function pulls the current discussion settings
* option values.
*
* @param array $params Passing specific params for getting current discussion settings
* @return array
*/
public function get_settings($params) {
global $updraftcentral_main;
// Here, we're getting the current blog id. If blog id
// is passed along with the parameters then we override
// that current (default) value with the parameter blog id value.
$blog_id = get_current_blog_id();
if (isset($params['blog_id'])) $blog_id = $params['blog_id'];
// If user does not have sufficient privileges to manage and edit
// WP options then we return with error.
if (!current_user_can_for_blog($blog_id, 'manage_options')) {
$result = array('error' => true, 'message' => 'insufficient_permission');
return $this->_response($result);
}
// Pull sites options if available.
$sites = $this->get_blog_sites();
// Wrap current discussion settings values into an array item
// named settings.
$result = array(
'settings' => array(
'default_pingback_flag' => $this->_get_wp_option($blog_id, 'default_pingback_flag'),
'default_ping_status' => $this->_get_wp_option($blog_id, 'default_ping_status'),
'default_comment_status' => $this->_get_wp_option($blog_id, 'default_comment_status'),
'require_name_email' => $this->_get_wp_option($blog_id, 'require_name_email'),
'comment_registration' => $this->_get_wp_option($blog_id, 'comment_registration'),
'close_comments_for_old_posts' => $this->_get_wp_option($blog_id, 'close_comments_for_old_posts'),
'close_comments_days_old' => $this->_get_wp_option($blog_id, 'close_comments_days_old'),
'thread_comments' => $this->_get_wp_option($blog_id, 'thread_comments'),
'thread_comments_depth' => $this->_get_wp_option($blog_id, 'thread_comments_depth'),
'page_comments' => $this->_get_wp_option($blog_id, 'page_comments'),
'comments_per_page' => $this->_get_wp_option($blog_id, 'comments_per_page'),
'default_comments_page' => $this->_get_wp_option($blog_id, 'default_comments_page'),
'comment_order' => $this->_get_wp_option($blog_id, 'comment_order'),
'comments_notify' => $this->_get_wp_option($blog_id, 'comments_notify'),
'moderation_notify' => $this->_get_wp_option($blog_id, 'moderation_notify'),
'comment_moderation' => $this->_get_wp_option($blog_id, 'comment_moderation'),
'comment_max_links' => $this->_get_wp_option($blog_id, 'comment_max_links'),
'moderation_keys' => $this->_get_wp_option($blog_id, 'moderation_keys'),
),
'sites' => $sites,
);
$wp_version = $updraftcentral_main->get_wordpress_version();
if (version_compare($wp_version, '5.5.0', '<')) {
$result['settings']['comment_whitelist'] = $this->_get_wp_option($blog_id, 'comment_whitelist');
$result['settings']['blacklist_keys'] = $this->_get_wp_option($blog_id, 'blacklist_keys');
} else {
$result['settings']['comment_previously_approved'] = $this->_get_wp_option($blog_id, 'comment_previously_approved');
$result['settings']['disallowed_keys'] = $this->_get_wp_option($blog_id, 'disallowed_keys');
}
return $this->_response($result);
}
/**
* The update_settings function updates the discussion settings
* basing on the user generated content/option from the frontend
* form.
*
* @param array $params Specific params to update settings based on discussion
* @return array
*/
public function update_settings($params) {
// Extract settings values from passed parameters.
$settings = $params['settings'];
// Here, we're getting the current blog id. If blog id
// is passed along with the parameters then we override
// that current (default) value with the parameter blog id value.
$blog_id = get_current_blog_id();
if (isset($params['blog_id'])) $blog_id = $params['blog_id'];
// If user does not have sufficient privileges to manage and edit
// WP options then we return with error.
if (!current_user_can_for_blog($blog_id, 'manage_options')) {
$result = array('error' => true, 'message' => 'insufficient_permission');
return $this->_response($result);
}
// Here, we're sanitizing the input fields before we save them to the database
// for safety and security reason. The "explode" and "implode" functions are meant
// to maintain the line breaks associated with a textarea input/value.
foreach ($settings as $key => $value) {
// We're using update_blog_option and update_option altogether to update the current
// discussion settings.
if (is_multisite()) {
update_blog_option($blog_id, $key, implode("\n", array_map('sanitize_text_field', explode("\n", $value))));
} else {
update_option($key, implode("\n", array_map('sanitize_text_field', explode("\n", $value))));
}
}
// We're not checking for errors here, but instead we're directly returning a success (error = false)
// status always, because WP's update_option will return fail if values were not changed, meaning
// previous values were not changed by the user's current request, not an actual exception thrown.
// Thus, giving a false positive message or report to the frontend.
$result = array('error' => false, 'message' => 'settings_updated', 'values' => array());
return $this->_response($result);
}
/**
* The get_comment function pulls a single comment based
* on a comment ID.
*
* @param array $params Specific params for getting a single comment
* @return array
*/
public function get_comment($params) {
// Here, we're getting the current blog id. If blog id
// is passed along with the parameters then we override
// that current (default) value with the parameter blog id value.
$blog_id = get_current_blog_id();
if (isset($params['blog_id'])) $blog_id = $params['blog_id'];
// If user does not have sufficient privileges to moderate or edit
// a comment then we return with error.
if (!current_user_can_for_blog($blog_id, 'moderate_comments')) {
$result = array('error' => true, 'message' => 'insufficient_permission');
return $this->_response($result);
}
// Here, we're switching to the actual blog that we need
// to pull comments from.
$switched = false;
if (function_exists('switch_to_blog')) {
$switched = switch_to_blog($blog_id);
}
// Get comment by comment_ID parameter and return result as an array.
$result = array(
'comment' => get_comment($params['comment_id'], ARRAY_A)
);
// Here, we're restoring to the current (default) blog before we
// do the switched.
if (function_exists('restore_current_blog') && $switched) {
restore_current_blog();
}
return $this->_response($result);
}
/**
* The reply_comment function creates a new comment as a reply
* to a certain/selected comment.
*
* @param array $params Specific params to create a new comment reply
* @return array
*/
public function reply_comment($params) {
// Extract reply info from the passed parameters
$reply = $params['comment'];
// Here, we're getting the current blog id. If blog id
// is passed along with the parameters then we override
// that current (default) value with the parameter blog id value.
$blog_id = get_current_blog_id();
if (isset($params['blog_id'])) $blog_id = $params['blog_id'];
// If user does not have sufficient privileges to moderate or edit
// a comment then we return with error.
if (!current_user_can_for_blog($blog_id, 'moderate_comments')) {
$result = array('error' => true, 'message' => 'comment_reply_no_permission');
return $this->_response($result);
}
// Here, we're switching to the actual blog that we need
// to apply our changes.
$switched = false;
if (function_exists('switch_to_blog')) {
$switched = switch_to_blog($blog_id);
}
// Get comment by comment_ID parameter.
$comment = get_comment($reply['comment_id']);
if ($comment) {
// Get the currently logged in user
$user = wp_get_current_user();
// If the current comment was not approved yet then
// we need to approve it before we create a reply to
// to the comment, mimicking exactly the WP behaviour
// in terms of creating a reply to a comment.
if (empty($comment->comment_approved)) {
$update_data = array(
'comment_ID' => $reply['comment_id'],
'comment_approved' => 1
);
wp_update_comment($update_data);
}
// Build new comment parameters based on current user info and
// the target comment for the reply.
$data = array(
'comment_post_ID' => $comment->comment_post_ID,
'comment_author' => $user->display_name,
'comment_author_email' => $user->user_email,
'comment_author_url' => $user->user_url,
'comment_content' => $reply['message'],
'comment_parent' => $reply['comment_id'],
'user_id' => $user->ID,
'comment_date' => current_time('mysql'),
'comment_approved' => 1
);
// Create new comment based on the parameters above, and return
// the status accordingly.
if (wp_insert_comment($data)) {
$result = array('error' => false, 'message' => 'comment_replied_with_comment_author', 'values' => array($comment->comment_author));
} else {
$result = array('error' => true, 'message' => 'comment_reply_failed_with_error', 'values' => array($comment->comment_ID));
}
} else {
$result = array('error' => true, 'message' => 'comment_does_not_exists_error', 'values' => array($reply['comment_id']));
}
// Here, we're restoring to the current (default) blog before we
// do the switched.
if (function_exists('restore_current_blog') && $switched) {
restore_current_blog();
}
return $this->_response($result);
}
/**
* The edit_comment function saves new information for the
* currently selected comment.
*
* @param array $params Specific params for editing a comment
* @return array
*/
public function edit_comment($params) {
// Extract new comment info from the passed parameters
$comment = $params['comment'];
// Here, we're getting the current blog id. If blog id
// is passed along with the parameters then we override
// that current (default) value with the parameter blog id value.
$blog_id = get_current_blog_id();
if (isset($params['blog_id'])) $blog_id = $params['blog_id'];
// If user does not have sufficient privileges to moderate or edit
// a comment then we return with error.
if (!current_user_can_for_blog($blog_id, 'moderate_comments')) {
$result = array('error' => true, 'message' => 'comment_edit_no_permission');
return $this->_response($result);
}
// Here, we're switching to the actual blog that we need
// to apply our changes.
$switched = false;
if (function_exists('switch_to_blog')) {
$switched = switch_to_blog($blog_id);
}
// Get current comment details
$original_comment = get_comment($comment['comment_id']);
if ($original_comment) {
$data = array();
// Replace "comment_id" with "comment_ID" since WP does not recognize
// the small case "id".
$comment['comment_ID'] = $original_comment->comment_ID;
unset($comment['comment_id']);
// Here, we're sanitizing the input fields before we save them to the database
// for safety and security reason. The "explode" and "implode" functions are meant
// to maintain the line breaks associated with a textarea input/value.
foreach ($comment as $key => $value) {
$data[$key] = implode("\n", array_map('sanitize_text_field', explode("\n", $value)));
}
// Update existing comment based on the passed parameter fields and
// return the status accordingly.
if (wp_update_comment($data)) {
$result = array('error' => false, 'message' => 'comment_edited_with_comment_author', 'values' => array($original_comment->comment_author));
} else {
$result = array('error' => true, 'message' => 'comment_edit_failed_with_error', 'values' => array($original_comment->comment_ID));
}
} else {
$result = array('error' => true, 'message' => 'comment_does_not_exists_error', 'values' => array($comment['comment_id']));
}
// Here, we're restoring to the current (default) blog before we
// do the switched.
if (function_exists('restore_current_blog') && $switched) {
restore_current_blog();
}
return $this->_response($result);
}
/**
* The update_comment_status function is a generic handler for the following
* comment actions:
*
* - approve comment
* - unapprove comment
* - set comment as spam
* - move comment to trash
* - delete comment permanently
* - unset comment as spam
* - restore comment
*
* @param array $params Specific params to update comment status
* @return array
*/
public function update_comment_status($params) {
// Here, we're getting the current blog id. If blog id
// is passed along with the parameters then we override
// that current (default) value with the parameter blog id value.
$blog_id = get_current_blog_id();
if (isset($params['blog_id'])) $blog_id = $params['blog_id'];
// If user does not have sufficient privileges to moderate or edit
// a comment then we return with error.
if (!current_user_can_for_blog($blog_id, 'moderate_comments')) {
$result = array('error' => true, 'message' => 'comment_change_status_no_permission');
return $this->_response($result);
}
// Here, we're switching to the actual blog that we need
// to apply our changes.
$switched = false;
if (function_exists('switch_to_blog')) {
$switched = switch_to_blog($blog_id);
}
// We make sure that we still have a valid comment from the server
// before we apply the currently selected action.
$comment = get_comment($params['comment_id']);
if ($comment) {
$post = get_post($comment->comment_post_ID);
if ($post) $comment->in_response_to = $post->post_title;
if (!empty($comment->comment_parent)) {
$parent_comment = get_comment($comment->comment_parent);
if ($parent_comment) $comment->in_reply_to = $parent_comment->comment_author;
}
// We're formatting the comment_date to be exactly the same
// with that of WP Comments table (e.g. 2016/12/21 at 10:30 PM)
$comment->comment_date = date('Y/m/d \a\t g:i a', strtotime($comment->comment_date));
$status = wp_get_comment_status($comment->comment_ID);
if ($status) {
$comment->comment_status = $status;
}
$succeeded = false;
$message = '';
// Here, we're using WP's wp_set_comment_status function to change the state
// of the selected comment based on the current action, except for the "delete" action
// where we use the wp_delete_comment to delete the comment permanently by passing
// "true" to the second argument.
switch ($params['action']) {
case 'approve':
$succeeded = wp_set_comment_status($params['comment_id'], 'approve');
$message = 'comment_approve_with_comment_author';
break;
case 'unapprove':
$succeeded = wp_set_comment_status($params['comment_id'], 'hold');
$message = 'comment_unapprove_with_comment_author';
break;
case 'spam':
$succeeded = wp_set_comment_status($params['comment_id'], 'spam');
$message = 'comment_spam_with_comment_author';
break;
case 'trash':
$succeeded = wp_set_comment_status($params['comment_id'], 'trash');
$message = 'comment_trash_with_comment_author';
break;
case 'delete':
$succeeded = wp_delete_comment($params['comment_id'], true);
$message = 'comment_delete_with_comment_author';
break;
case 'notspam':
$succeeded = wp_set_comment_status($params['comment_id'], 'hold');
$message = 'comment_not_spam_with_comment_author';
break;
case 'restore':
$succeeded = wp_set_comment_status($params['comment_id'], 'hold');
$message = 'comment_restore_with_comment_author';
break;
}
// If the current action succeeded, then we return a success message, otherwise,
// we return an error message to the user issuing the request.
if ($succeeded) {
$result = array('error' => false, 'message' => $message, 'values' => array($comment->comment_author), 'status' => $comment->comment_status, 'approved' => $comment->comment_approved);
} else {
$result = array('error' => true, 'message' => 'comment_change_status_failed_with_error', 'values' => array($comment->comment_ID));
}
} else {
$result = array('error' => true, 'message' => 'comment_does_not_exists_error', 'values' => array($params['comment_id']));
}
// Here, we're restoring to the current (default) blog before we
// do the switched.
if (function_exists('restore_current_blog') && $switched) {
restore_current_blog();
}
return $this->_response($result);
}
}

View File

@@ -0,0 +1,653 @@
<?php
if (!defined('UPDRAFTCENTRAL_CLIENT_DIR')) die('No access.');
/**
* - A container for RPC commands (core UpdraftCentral commands). Commands map exactly onto method names (and hence this class should not implement anything else, beyond the constructor, and private methods)
* - Return format is array('response' => (string - a code), 'data' => (mixed));
*
* RPC commands are not allowed to begin with an underscore. So, any private methods can be prefixed with an underscore.
*/
class UpdraftCentral_Core_Commands extends UpdraftCentral_Commands {
/**
* Retrieve site icon (favicon)
*
* @return array An array containing the site icon (favicon) byte string if available
*/
public function get_site_icon() {
if (!function_exists('get_site_icon_url')) {
include_once(ABSPATH.'wp-includes/general-template.php');
}
if (!class_exists('UpdraftCentral_Media_Commands') && defined('UPDRAFTCENTRAL_CLIENT_DIR')) {
include_once(UPDRAFTCENTRAL_CLIENT_DIR.'/modules/media.php');
}
$media = new UpdraftCentral_Media_Commands($this);
$icon_details = array();
$site_icon_url = get_site_icon_url();
// If none is set in WordPress, let's try to search for the default favicon
// within the site's directory
if (empty($site_icon_url)) {
if (!function_exists('get_site_url')) {
include_once(ABSPATH.'wp-includes/link-template.php');
}
// Common favicon locations to check
$potential_locations = array(
'/favicon.ico',
'/favicon.png',
'/favicon.svg',
'/assets/favicon.ico',
'/assets/images/favicon.ico',
'/apple-touch-icon.png',
'/apple-touch-icon-precomposed.png',
);
foreach ($potential_locations as $location) {
$path = rtrim(ABSPATH, '/\\').$location;
if (file_exists($path)) {
$site_icon_url = get_site_url().$location;
break;
}
}
} else {
$site_icon_id = (int) get_option('site_icon');
if ($site_icon_id) $icon_details = $media->get_media_item(array('id' => $site_icon_id), null, true);
}
// We are returning the site icon as byte string instead of URL in order to avoid
// any hotlink protection that might prevent us to show the icon in UpdraftCentral
// dashboard successfully.
$site_icon = '';
if (!empty($site_icon_url)) {
$content = file_get_contents($site_icon_url);
$mime_type = '';
foreach ($http_response_header as $value) {
if (false !== stripos($value, 'content-type:')) {
list(, $mime_type) = explode(':', preg_replace('/\s+/', '', $value));
break;
}
}
if ($content && !empty($mime_type)) {
$site_icon = 'data: '.$mime_type.';base64,'.base64_encode($content);
}
}
return $this->_response(array('site_icon' => $site_icon, 'icon_details' => $icon_details));
}
/**
* Handles site icon upload
*
* @param Array $query An array containing the image data.
*
* @return Array
*/
public function handle_site_icon_upload($query) {
if (!current_user_can('upload_files')) {
return $this->_generic_error_response('insufficient_permission', array('error_message' => __('You do not have the necessary permissions to upload files.', 'updraftcentral')));
}
$data_uri = sanitize_text_field($query['data_uri']);
$filename = sanitize_text_field(basename($query['filename']));
$file_ext = pathinfo($filename, PATHINFO_EXTENSION);
$history = sanitize_text_field($query['history']);
$preview = sanitize_text_field($query['preview']);
$pattern = '/^data:(image\/[^;]+);base64,([A-Za-z0-9+\/=]+)$/i';
if (!empty($data_uri) && preg_match($pattern, $data_uri, $matches)) {
list(, $data) = explode(';', $data_uri);
list(, $data) = explode(',', $data);
$decoded_data = base64_decode($data);
if (!class_exists('wp_tempnam')) include_once(ABSPATH.'/wp-admin/includes/file.php');
$temp_img_file = wp_tempnam();
@file_put_contents($temp_img_file, $decoded_data); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise because of the call.
$mime_type = wp_get_image_mime($temp_img_file);
@unlink($temp_img_file); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise if the file doesn't exist.
$allowed_ext = array('jpg', 'jpeg', 'png', 'gif', 'bmp', 'tif', 'webp', 'avif', 'heic');
if (false !== $mime_type && !empty($matches[1]) && strtolower($mime_type) === strtolower($matches[1]) && !empty($file_ext) && in_array(strtolower($file_ext), $allowed_ext)) {
$upload = wp_upload_bits($filename, null, $decoded_data);
} else {
$upload = array('error' => __("Couldn't verify the actual MIME type of the given site icon image data.", 'updraftcentral'));
}
if (!$upload['error']) {
$attachment = array(
'guid' => $upload['url'],
'post_mime_type' => $upload['type'],
'post_title' => sanitize_file_name($filename),
'post_content' => '',
'post_status' => 'inherit',
);
$attach_id = wp_insert_attachment($attachment, $upload['file']);
if (!function_exists('wp_generate_attachment_metadata')) {
require_once(ABSPATH.'wp-admin/includes/image.php');
}
// Generate metadata and thumbnails
$attach_data = wp_generate_attachment_metadata($attach_id, $upload['file']);
wp_update_attachment_metadata($attach_id, $attach_data);
if (update_option('site_icon', $attach_id)) {
if (1 == intval($preview)) {
if (!class_exists('UpdraftCentral_Media_Commands') && defined('UPDRAFTCENTRAL_CLIENT_DIR')) {
include_once(UPDRAFTCENTRAL_CLIENT_DIR.'/modules/media.php');
}
$media = new UpdraftCentral_Media_Commands($this);
$icon_details = $media->get_media_item(array('id' => $attach_id), null, true);
$params = array(
'_ajax_nonce' => $icon_details->nonce,
'postid' => $attach_id,
'history' => $history,
'rand' => $icon_details->misc['rand'],
);
$result = $media->image_preview($params);
$result['data']['icon_details'] = $icon_details;
return $result;
} else {
return $this->get_site_icon();
}
} else {
return $this->_generic_error_response('upload_error', array('error_message' => __('Unable to set uploaded file as site icon.', 'updraftcentral')));
}
} else {
return $this->_generic_error_response('upload_error', array('error_message' => $upload['error']));
}
} else {
return $this->_generic_error_response('data_uri_field_empty_or_invalid', array('error_message' => __('Required data URI is either missing or invalid.', 'updraftcentral')));
}
}
/**
* Pulls blog sites available
* for the current WP instance.
* If the site is a multisite, then sites under the network
* will be pulled, otherwise, it will return an empty array.
*
* @return Array - an array of sites
*/
public function get_blog_sites() {
if (!is_multisite()) {
return $this->_generic_error_response('not_multisite');
}
$sites = array();
$network_sites = array();
// Use `get_sites` for WP version >= 4.6 else use old `wp_get_sites`.
if (function_exists('get_sites') && class_exists('WP_Site_Query')) {
$network_sites = get_sites();
} else {
if (function_exists('wp_get_sites')) {
$network_sites = wp_get_sites();
}
}
if (!empty($network_sites)) {
foreach ($network_sites as $site) {
// Check if the site type is an array,
// because `wp_get_sites` returns site data as associative array while,
// `get_sites` returns the data as WP_Site object.
$blog_id = is_array($site) ? $site['blog_id'] : $site->blog_id;
$sites[] = array(
'site_id' => $blog_id,
'name' => get_blog_details($blog_id)->blogname,
);
}
}
return $this->_response(array(
'sites' => $sites,
));
}
/**
* Executes a list of submitted commands (multiplexer)
*
* @param Array $query An array containing the commands to execute and a flag to indicate how to handle command execution failure.
* @return Array An array containing the results of the process.
*/
public function execute_commands($query) {
try {
$commands = $query['commands'];
$command_results = array();
$error_count = 0;
/**
* Should be one of the following options:
* 1 = Abort on first failure
* 2 = Abort if any command fails
* 3 = Abort if all command fails (default)
*/
$error_flag = isset($query['error_flag']) ? (int) $query['error_flag'] : 3;
foreach ($commands as $command => $params) {
$command_info = apply_filters('updraftcentral_get_command_info', false, $command);
if (!$command_info) {
list($_prefix, $_command) = explode('.', $command);
$command_results[$_prefix][$_command] = array('response' => 'rpcerror', 'data' => array('code' => 'unknown_rpc_command', 'data' => $command));
$error_count++;
if (1 === $error_flag) break;
} else {
$action = $command_info['command'];
$command_php_class = $command_info['command_php_class'];
// Instantiate the command class and execute the needed action
if (class_exists($command_php_class)) {
$instance = new $command_php_class($this->rc);
if (method_exists($instance, $action) || is_a($instance, 'UpdraftCentral_UpdraftPlus_Commands') || is_a($instance, 'UpdraftCentral_WP_Optimize_Commands')) {
$params = empty($params) ? array() : $params;
$call_result = call_user_func(array($instance, $action), $params);
$command_results[$command] = $call_result;
if ('rpcerror' === $call_result['response'] || (isset($call_result['data']['error']) && $call_result['data']['error'])) {
$error_count++;
if (1 === $error_flag) break;
}
}
}
}
}
if (0 !== $error_count) {
// N.B. These error messages should be defined in UpdraftCentral's translation file (dashboard-translations.php)
// before actually using this multiplexer function.
$message = 'general_command_execution_error';
switch ($error_flag) {
case 1:
$message = 'command_execution_aborted';
break;
case 2:
$message = 'failed_to_execute_some_commands';
break;
case 3:
if (count($commands) === $error_count) {
$message = 'failed_to_execute_all_commands';
}
break;
default:
break;
}
$result = array('error' => true, 'message' => $message, 'values' => $command_results);
} else {
$result = $command_results;
}
} catch (Exception $e) {
$result = array('error' => true, 'message' => $e->getMessage());
}
return $this->_response($result);
}
/**
* Validates the credentials entered by the user
*
* @param array $creds an array of filesystem credentials
* @return array An array containing the result of the validation process.
*/
public function validate_credentials($creds) {
try {
$entity = $creds['entity'];
if (isset($creds['filesystem_credentials'])) {
parse_str($creds['filesystem_credentials'], $filesystem_credentials);
if (is_array($filesystem_credentials)) {
foreach ($filesystem_credentials as $key => $value) {
// Put them into $_POST, which is where request_filesystem_credentials() checks for them.
$_POST[$key] = $value;
}
}
}
// Include the needed WP Core file(s)
// template.php needed for submit_button() which is called by request_filesystem_credentials()
$this->_admin_include('file.php', 'template.php');
// Directory entities that we currently need permissions
// to update.
$entity_directories = array(
'plugins' => WP_PLUGIN_DIR,
'themes' => WP_CONTENT_DIR.'/themes',
'core' => untrailingslashit(ABSPATH)
);
if ('translations' === $entity) {
// 'en_US' don't usually have the "languages" folder, thus, we
// check if there's a need to ask for filesystem credentials for that
// folder if it exists, most especially for locale other than 'en_US'.
$language_dir = WP_CONTENT_DIR.'/languages';
if ('en_US' !== get_locale() && is_dir($language_dir)) {
$entity_directories['translations'] = $language_dir;
}
}
$url = wp_nonce_url(site_url());
$passed = false;
if (isset($entity_directories[$entity])) {
$directory = $entity_directories[$entity];
// Check if credentials are valid and have sufficient
// privileges to create and delete (e.g. write)
ob_start();
$credentials = request_filesystem_credentials($url, '', false, $directory);
ob_end_clean();
// The "WP_Filesystem" will suffice in validating the inputted credentials
// from UpdraftCentral, as it is already attempting to connect to the filesystem
// using the chosen transport (e.g. ssh, ftp, etc.)
$passed = WP_Filesystem($credentials, $directory);
}
if ($passed) {
$result = array('error' => false, 'message' => 'credentials_ok', 'values' => array());
} else {
// We're adding some useful error information to help troubleshooting any problems
// that may arise in the future. If the user submitted a wrong password or username
// it usually falls through here.
global $wp_filesystem;
$errors = array();
if (isset($wp_filesystem->errors) && is_wp_error($wp_filesystem->errors)) {
$errors = $wp_filesystem->errors->errors;
}
$result = array('error' => true, 'message' => 'failed_credentials', 'values' => array('errors' => $errors));
}
} catch (Exception $e) {
$result = array('error' => true, 'message' => $e->getMessage(), 'values' => array());
}
return $this->_response($result);
}
/**
* Gets the FileSystem Credentials
*
* Extract the needed filesystem credentials (permissions) to be used
* to update/upgrade the plugins, themes and the WP core.
*
* @return array $result - An array containing the creds form and some flags
* to determine whether we need to extract the creds
* manually from the user.
*/
public function get_credentials() {
try {
// Check whether user has enough permission to update entities
if (!current_user_can('update_plugins') && !current_user_can('update_themes') && !current_user_can('update_core')) return $this->_generic_error_response('updates_permission_denied');
// Include the needed WP Core file(s)
$this->_admin_include('file.php', 'template.php');
// A container that will hold the state (in this case, either true or false) of
// each directory entities (plugins, themes, core) that will be used to determine
// whether or not there's a need to show a form that will ask the user for their credentials
// manually.
$request_filesystem_credentials = array();
// A container for the filesystem credentials form if applicable.
$filesystem_form = '';
// Directory entities that we currently need permissions
// to update.
$check_fs = array(
'plugins' => WP_PLUGIN_DIR,
'themes' => WP_CONTENT_DIR.'/themes',
'core' => untrailingslashit(ABSPATH)
);
// Here, we're looping through each entities and find output whether
// we have sufficient permissions to update objects belonging to them.
foreach ($check_fs as $entity => $dir) {
// We're determining which method to use when updating
// the files in the filesystem.
$filesystem_method = get_filesystem_method(array(), $dir);
// Buffering the output to pull the actual credentials form
// currently being used by this WP instance if no sufficient permissions
// is found.
$url = wp_nonce_url(site_url());
ob_start();
$filesystem_credentials_are_stored = request_filesystem_credentials($url, $filesystem_method);
$form = strip_tags(ob_get_contents(), '<div><h2><p><input><label><fieldset><legend><span><em>');
if (!empty($form)) {
$filesystem_form = $form;
}
ob_end_clean();
// Save the state whether or not there's a need to show the
// credentials form to the user.
$request_filesystem_credentials[$entity] = ('direct' !== $filesystem_method && !$filesystem_credentials_are_stored);
}
// Wrapping the credentials info before passing it back
// to the client issuing the request.
$result = array(
'request_filesystem_credentials' => $request_filesystem_credentials,
'filesystem_form' => $filesystem_form
);
} catch (Exception $e) {
$result = array('error' => true, 'message' => $e->getMessage(), 'values' => array());
}
return $this->_response($result);
}
/**
* Fetches a browser-usable URL which will automatically log the user in to the site
*
* @param String $redirect_to - the URL to got to after logging in
* @param Array $extra_info - valid keys are user_id, which should be a numeric user ID to log in as.
*/
public function get_login_url($redirect_to, $extra_info) {
if (is_array($extra_info) && !empty($extra_info['user_id']) && is_numeric($extra_info['user_id'])) {
$user_id = $extra_info['user_id'];
if (false == ($login_key = $this->_get_autologin_key($user_id))) return $this->_generic_error_response('user_key_failure');
// Default value
$redirect_url = network_admin_url();
if (is_array($redirect_to) && !empty($redirect_to['module'])) {
switch ($redirect_to['module']) {
case 'updraftplus':
if ('initiate_restore' == $redirect_to['action'] && class_exists('UpdraftPlus_Options')) {
$redirect_url = UpdraftPlus_Options::admin_page_url().'?page=updraftplus&udaction=initiate_restore&entities='.urlencode($redirect_to['data']['entities']).'&showdata='.urlencode($redirect_to['data']['showdata']).'&backup_timestamp='.(int) $redirect_to['data']['backup_timestamp'];
} elseif ('download_file' == $redirect_to['action']) {
$findex = empty($redirect_to['data']['findex']) ? 0 : (int) $redirect_to['data']['findex'];
// e.g. ?udcentral_action=dl&action=updraftplus_spool_file&backup_timestamp=1455101696&findex=0&what=plugins
$redirect_url = site_url().'?udcentral_action=spool_file&action=updraftplus_spool_file&findex='.$findex.'&what='.urlencode($redirect_to['data']['what']).'&backup_timestamp='.(int) $redirect_to['data']['backup_timestamp'];
}
break;
case 'direct_url':
$redirect_url = $redirect_to['url'];
break;
}
}
$login_key = apply_filters('updraftplus_remotecontrol_login_key', array(
'key' => $login_key,
'created' => time(),
'redirect_url' => $redirect_url
), $redirect_to, $extra_info);
// Over-write any previous value - only one can be valid at a time)
update_user_meta($user_id, 'updraftcentral_login_key', $login_key);
return $this->_response(array(
'login_url' => network_site_url('?udcentral_action=login&login_id='.$user_id.'&login_key='.$login_key['key'])
));
} else {
return $this->_generic_error_response('user_unknown');
}
}
/**
* Get information derived from phpinfo()
*
* @return Array
*/
public function phpinfo() {
$phpinfo = $this->_get_phpinfo_array();
if (!empty($phpinfo)) {
return $this->_response($phpinfo);
}
return $this->_generic_error_response('phpinfo_fail');
}
/**
* The key obtained is only intended to be short-lived. Hence, there's no intention other than that it is random and only used once - only the most recent one is valid.
*
* @param Integer $user_id Specific user ID to get the autologin key
* @return Array
*/
public function _get_autologin_key($user_id) {
$secure_auth_key = defined('SECURE_AUTH_KEY') ? SECURE_AUTH_KEY : hash('sha256', DB_PASSWORD).'_'.rand(0, 999999999);
if (!defined('SECURE_AUTH_KEY')) return false;
$hash_it = $user_id.'_'.microtime(true).'_'.rand(0, 999999999).'_'.$secure_auth_key;
$hash = hash('sha256', $hash_it);
return $hash;
}
public function site_info() {
global $wpdb;
// THis is included so we can get $wp_version
@include(ABSPATH.WPINC.'/version.php');// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise because of the function.
$ud_version = is_a($this->ud, 'UpdraftPlus') ? $this->ud->version : 'none';
return $this->_response(array(
'versions' => array(
'ud' => $ud_version,
'php' => PHP_VERSION,
'wp' => $wp_version,// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable -- The variable is defined inside the ABSPATH.WPINC.'/version.php'.
'mysql' => $wpdb->db_version(),
'udrpc_php' => $this->rc->udrpc_version,
),
'bloginfo' => array(
'url' => network_site_url(),
'name' => get_bloginfo('name'),
)
));
}
/**
* This calls the WP_Action within WP
*
* @param array $data Array of Data to be used within call_wp_action
* @return array
*/
public function call_wordpress_action($data) {
if (false === ($updraftplus_admin = $this->_load_ud_admin())) return $this->_generic_error_response('no_updraftplus');
$response = $updraftplus_admin->call_wp_action($data);
if (empty($data["wpaction"])) {
return $this->_generic_error_response("error", "no command sent");
}
return $this->_response(array(
"response" => $response['response'],
"status" => $response['status'],
"log" => $response['log']
));
}
/**
* Get disk space used
*
* @uses UpdraftPlus_Filesystem_Functions::get_disk_space_used()
*
* @param String $entity - the entity to count (e.g. 'plugins', 'themes')
*
* @return Array - response
*/
public function count($entity) {
if (!class_exists('UpdraftPlus_Filesystem_Functions')) return $this->_generic_error_response('no_updraftplus');
$response = UpdraftPlus_Filesystem_Functions::get_disk_space_used($entity);
return $this->_response($response);
}
/**
* https://secure.php.net/phpinfo
*
* @return null|array
*/
private function _get_phpinfo_array() {
if (!function_exists('phpinfo')) return null;
ob_start();
phpinfo(INFO_GENERAL|INFO_CREDITS|INFO_MODULES);
$phpinfo = array('phpinfo' => array());
if (preg_match_all('#(?:<h2>(?:<a name=".*?">)?(.*?)(?:</a>)?</h2>)|(?:<tr(?: class=".*?")?><t[hd](?: class=".*?")?>(.*?)\s*</t[hd]>(?:<t[hd](?: class=".*?")?>(.*?)\s*</t[hd]>(?:<t[hd](?: class=".*?")?>(.*?)\s*</t[hd]>)?)?</tr>)#s', ob_get_clean(), $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
if (strlen($match[1])) {
$phpinfo[$match[1]] = array();
} elseif (isset($match[3])) {
$keys1 = array_keys($phpinfo);
$phpinfo[end($keys1)][$match[2]] = isset($match[4]) ? array($match[3], $match[4]) : $match[3];
} else {
$keys1 = array_keys($phpinfo);
$phpinfo[end($keys1)][] = $match[2];
}
}
return $phpinfo;
}
return false;
}
/**
* Return an UpdraftPlus_Admin object
*
* @return UpdraftPlus_Admin|Boolean - false in case of failure
*/
private function _load_ud_admin() {
if (!defined('UPDRAFTPLUS_DIR') || !is_file(UPDRAFTPLUS_DIR.'/admin.php')) return false;
updraft_try_include_file('admin.php', 'include_once');
global $updraftplus_admin;
return $updraftplus_admin;
}
}

View File

@@ -0,0 +1,601 @@
<?php
if (!defined('UPDRAFTCENTRAL_CLIENT_DIR')) die('No access.');
/**
* Handles Media Commands
*/
class UpdraftCentral_Media_Commands extends UpdraftCentral_Commands {
private $switched = false;
/**
* Function that gets called before every action
*
* @param string $command a string that corresponds to UDC command to call a certain method for this class.
* @param array $data an array of data post or get fields
* @param array $extra_info extrainfo use in the udrpc_action, e.g. user_id
*
* link to udrpc_action main function in class UpdraftCentral_Listener
*/
public function _pre_action($command, $data, $extra_info) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- This function is called from listener.php and $extra_info is being sent.
// Here we assign the current blog_id to a variable $blog_id
$blog_id = get_current_blog_id();
if (!empty($data['site_id'])) $blog_id = $data['site_id'];
if (function_exists('switch_to_blog') && is_multisite() && $blog_id) {
$this->switched = switch_to_blog($blog_id);
}
}
/**
* Function that gets called after every action
*
* @param string $command a string that corresponds to UDC command to call a certain method for this class.
* @param array $data an array of data post or get fields
* @param array $extra_info extrainfo use in the udrpc_action, e.g. user_id
*
* link to udrpc_action main function in class UpdraftCentral_Listener
*/
public function _post_action($command, $data, $extra_info) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Unused parameter is present because the caller from UpdraftCentral_Listener class uses 3 arguments.
// Here, we're restoring to the current (default) blog before we switched
if ($this->switched) restore_current_blog();
}
/**
* Fetch and retrieves posts based from the submitted parameters
*
* @param array $params Containing all the needed information to filter the results of the current request
* @return array
*/
public function get_media_items($params) {
$error = $this->_validate_capabilities(array('upload_files', 'edit_posts'));
if (!empty($error)) return $error;
// check paged parameter; if empty set to defaults
$paged = !empty($params['paged']) ? (int) $params['paged'] : 1;
$numberposts = !empty($params['numberposts']) ? (int) $params['numberposts'] : 10;
$offset = ($paged - 1) * $numberposts;
$args = array(
'posts_per_page' => $numberposts,
'paged' => $paged,
'offset' => $offset,
'post_type' => 'attachment',
'post_status' => 'inherit',
);
if (!empty($params['keyword'])) {
$args['s'] = $params['keyword'];
}
if (!empty($params['category'])) {
if (in_array($params['category'], array('detached', 'unattached'))) {
$attachment_ids = $this->get_unattached_ids();
} else {
$attachment_ids = $this->get_type_ids($params['category']);
}
$args['post__in'] = $attachment_ids;
}
if (!empty($params['date'])) {
list($monthnum, $year) = explode(':', $params['date']);
$args['monthnum'] = $monthnum;
$args['year'] = $year;
}
$query = new WP_Query($args);
$result = $query->posts;
$count_posts = (int) $query->found_posts;
$page_count = 0;
if ($count_posts > 0) {
$page_count = absint($count_posts / $numberposts);
$remainder = absint($count_posts % $numberposts);
$page_count = ($remainder > 0) ? ++$page_count : $page_count;
}
$info = array(
'page' => $paged,
'pages' => $page_count,
'results' => $count_posts,
'items_from' => (($paged * $numberposts) - $numberposts) + 1,
'items_to' => ($paged == $page_count) ? $count_posts : $paged * $numberposts,
);
$media_items = array();
if (!empty($result)) {
foreach ($result as $item) {
$media = $this->get_media_item($item, null, true);
if (!empty($media)) {
array_push($media_items, $media);
}
}
}
$response = array(
'items' => $media_items,
'info' => $info,
'options' => array(
'date' => $this->get_date_options(),
'type' => $this->get_type_options()
)
);
return $this->_response($response);
}
/**
* Check whether we have an image editor (e.g. GD, Imagick, etc.) set in place to handle the basic editing
* functions such as rotate, crop, etc. If not, then we hide that feature in UpdraftCentral
*
* @param object $media The media item/object to check
* @return boolean
*/
private function has_image_editor($media) {
// Most of the time image library are enabled by default in the php.ini but there's a possbility that users don't
// enable them as they have no need for them at the moment or for some other reasons. Thus, we need to confirm
// that here through the wp_get_image_editor method.
$has_image_editor = true;
if (!empty($media)) {
if (!function_exists('wp_get_image_editor')) {
require_once(ABSPATH.'wp-includes/media.php');
}
if (!function_exists('_load_image_to_edit_path')) {
require_once(ABSPATH.'wp-admin/includes/image.php');
}
$image_editor = wp_get_image_editor(_load_image_to_edit_path($media->ID));
if (is_wp_error($image_editor)) {
$has_image_editor = false;
}
}
return $has_image_editor;
}
/**
* Fetch a single media item information
*
* @param array $params Containing all the needed information to filter the results of the current request
* @param array|null $extra_info Additional information from the current request
* @param boolean $raw If set, returns the result of the fetch process unwrapped by the response array
* @return array
*/
public function get_media_item($params, $extra_info = null, $raw = false) {
$error = $this->_validate_capabilities(array('upload_files', 'edit_posts'));
if (!empty($error)) return $error;
// Raw means that we need to return the result without wrapping it
// with the "$this->_response" function which indicates that the call
// was done locally (within the class) and not directly from UpdraftCentral.
if ($raw && is_object($params) && isset($params->ID)) {
$media = $params;
} elseif (is_array($params) && !empty($params['id'])) {
$media = get_post($params['id']);
}
if (!function_exists('get_post_mime_types')) {
global $updraftcentral_main;
// For a much later version of WP the "get_post_mime_types" is located
// in a different folder. So, we make sure that we have it loaded before
// actually using it.
if (version_compare($updraftcentral_main->get_wordpress_version(), '3.5', '>=')) {
require_once(ABSPATH.WPINC.'/post.php');
} else {
// For WP 3.4, the "get_post_mime_types" is located in the location provided below.
require_once(ABSPATH.'wp-admin/includes/post.php');
}
}
if (!function_exists('wp_image_editor')) {
require_once(ABSPATH.'wp-admin/includes/image-edit.php');
}
if (!function_exists('get_media_item')) {
require_once(ABSPATH.'wp-admin/includes/template.php');
require_once(ABSPATH.'wp-admin/includes/media.php');
}
if ($media) {
$thumb = wp_get_attachment_image_src($media->ID, 'thumbnail', true);
if (!empty($thumb)) $media->thumb_url = $thumb[0];
$media->url = wp_get_attachment_url($media->ID);
$media->parent_post_title = get_the_title($media->post_parent);
$media->author = get_the_author_meta('display_name', $media->post_author);
$media->filename = basename($media->url);
$media->date = date('Y/m/d', strtotime($media->post_date));
$media->upload_date = mysql2date(get_option('date_format'), $media->post_date);
$media->filesize = 0;
$file = get_attached_file($media->ID);
if (!empty($file) && file_exists($file)) {
$media->filesize = size_format(filesize($file));
}
$media->nonce = wp_create_nonce('image_editor-'.$media->ID);
if (false !== strpos($media->post_mime_type, 'image/')) {
$meta = wp_get_attachment_metadata($media->ID);
$thumb = image_get_intermediate_size($media->ID, 'thumbnail');
$sub_sizes = isset($meta['sizes']) && is_array($meta['sizes']);
// Pulling details
$sizer = 1;
if (isset($meta['width'], $meta['height'])) {
$big = max($meta['width'], $meta['height']);
$sizer = $big > 400 ? 400 / $big : 1;
}
$constrained_dims = array();
if ($thumb && $sub_sizes) {
$constrained_dims = wp_constrain_dimensions($thumb['width'], $thumb['height'], 160, 120);
}
$rotate_supported = false;
if (function_exists('imagerotate') || wp_image_editor_supports(array('mime_type' => get_post_mime_type($media->ID), 'methods' => array('rotate')))) {
$rotate_supported = true;
}
// Check for alternative text if present
$alt = get_post_meta($media->ID, '_wp_attachment_image_alt', true);
$media->alt = !empty($alt) ? $alt : '';
// Check whether edited images are restorable
$backup_sizes = get_post_meta($media->ID, '_wp_attachment_backup_sizes', true);
$can_restore = !empty($backup_sizes) && isset($backup_sizes['full-orig']) && basename($meta['file']) != $backup_sizes['full-orig']['file'];
$image_edit_overwrite = (!defined('IMAGE_EDIT_OVERWRITE') || !IMAGE_EDIT_OVERWRITE) ? 0 : 1;
$media->misc = array(
'sizer' => $sizer,
'rand' => rand(1, 99999),
'constrained_dims' => $constrained_dims,
'rotate_supported' => (int) $rotate_supported,
'thumb' => $thumb,
'meta' => $meta,
'alt_text' => $alt,
'can_restore' => $can_restore,
'image_edit_overwrite' => $image_edit_overwrite
);
$media->has_image_editor = $this->has_image_editor($media);
}
}
return $raw ? $media : $this->_response(array('item' => $media));
}
/**
* Fetch and retrieves posts based from the submitted parameters
*
* @param array $params Containing all the needed information to filter the results of the current request
* @return array
*/
public function get_posts($params) {
$error = $this->_validate_capabilities(array('edit_posts'));
if (!empty($error)) return $error;
// check paged parameter; if empty set to defaults
$paged = !empty($params['paged']) ? (int) $params['paged'] : 1;
$numberposts = !empty($params['numberposts']) ? (int) $params['numberposts'] : 10;
$offset = ($paged - 1) * $numberposts;
$args = array(
'posts_per_page' => $numberposts,
'paged' => $paged,
'offset' => $offset,
'post_type' => 'post',
'post_status' => 'publish,private,draft,pending,future',
);
if (!empty($params['keyword'])) {
$args['s'] = $params['keyword'];
}
$query = new WP_Query($args);
$result = $query->posts;
$count_posts = (int) $query->found_posts;
$page_count = 0;
if ($count_posts > 0) {
$page_count = absint($count_posts / $numberposts);
$remainder = absint($count_posts % $numberposts);
$page_count = ($remainder > 0) ? ++$page_count : $page_count;
}
$info = array(
'page' => $paged,
'pages' => $page_count,
'results' => $count_posts,
'items_from' => (($paged * $numberposts) - $numberposts) + 1,
'items_to' => ($paged == $page_count) ? $count_posts : $paged * $numberposts,
);
$posts = array();
if (!empty($result)) {
foreach ($result as $post) {
array_push($posts, array('ID' => $post->ID, 'title' => $post->post_title));
}
}
$response = array(
'posts' => $posts,
'info' => $info
);
return $this->_response($response);
}
/**
* Saves media changes from UpdraftCentral
*
* @param array $params Containing all the needed information to filter the results of the current request
* @return array
*/
public function save_media_item($params) {
$error = $this->_validate_capabilities(array('upload_files', 'edit_posts'));
if (!empty($error)) return $error;
$args = array(
'post_title' => $params['image_title'],
'post_excerpt' => $params['image_caption'],
'post_content' => $params['image_description']
);
if (!empty($params['new'])) {
$args['post_type'] = 'attachment';
$media_id = wp_insert_post($args, true);
} else {
$args['ID'] = $params['id'];
$args['post_modified'] = date('Y-m-d H:i:s');
$args['post_modified_gmt'] = gmdate('Y-m-d H:i:s');
$media_id = wp_update_post($args, true);
}
if (!empty($media_id)) {
// Update alternative text if not empty
if (!empty($params['image_alternative_text'])) {
update_post_meta($media_id, '_wp_attachment_image_alt', $params['image_alternative_text']);
}
$result = array(
'status' => 'success',
'item' => $this->get_media_item(array('id' => $media_id), null, true)
);
} else {
$result = array('status' => 'failed');
}
return $this->_response($result);
}
/**
* Executes media action (e.g. attach, detach and delete)
*
* @param array $params Containing all the needed information to filter the results of the current request
* @return array
*/
public function execute_media_action($params) {
global $updraftcentral_host_plugin;
$error = $this->_validate_capabilities(array('upload_files', 'edit_posts'));
if (!empty($error)) return $error;
$result = array();
switch ($params['do']) {
case 'attach':
global $wpdb;
$query_result = $wpdb->query($wpdb->prepare("UPDATE {$wpdb->posts} SET `post_parent` = %d WHERE `post_type` = 'attachment' AND ID = %d", $params['parent_id'], $params['id']));
if (false === $query_result) {
$result['error'] = $updraftcentral_host_plugin->retrieve_show_message('failed_to_attach_media');
} else {
$result['msg'] = $updraftcentral_host_plugin->retrieve_show_message('media_attached');
}
break;
case 'detach':
global $wpdb;
$query_result = $wpdb->query($wpdb->prepare("UPDATE {$wpdb->posts} SET `post_parent` = 0 WHERE `post_type` = 'attachment' AND ID = %d", $params['id']));
if (false === $query_result) {
$result['error'] = $updraftcentral_host_plugin->retrieve_show_message('failed_to_detach_media');
} else {
$result['msg'] = $updraftcentral_host_plugin->retrieve_show_message('media_detached');
}
break;
case 'delete':
$failed_items = array();
foreach ($params['ids'] as $id) {
// Delete permanently
if (false === wp_delete_attachment($id, true)) {
$failed_items[] = $id;
}
}
if (!empty($failed_items)) {
$result['error'] = $updraftcentral_host_plugin->retrieve_show_message('failed_to_delete_media');
$result['items'] = $failed_items;
} else {
$result['msg'] = $updraftcentral_host_plugin->retrieve_show_message('selected_media_deleted');
}
break;
default:
break;
}
return $this->_response($result);
}
/**
* Retrieves a collection of formatted dates found for the given post statuses.
* It will be used as options for the date filter when managing the media items in UpdraftCentral.
*
* @return array
*/
private function get_date_options() {
global $wpdb;
$options = array();
$date_options = $wpdb->get_col("SELECT DATE_FORMAT(`post_date`, '%M %Y') as `formatted_post_date` FROM {$wpdb->posts} WHERE `post_type` = 'attachment' AND `post_status` = 'inherit' GROUP BY `formatted_post_date` ORDER BY `post_date` DESC");
if (!empty($date_options)) {
foreach ($date_options as $monthyear) {
$timestr = strtotime($monthyear);
$options[] = array('label' => date('F Y', $timestr), 'value' => date('n:Y', $timestr));
}
}
return $options;
}
/**
* Retrieves mime types that will be use as filter option in UpdraftCentral
*
* @return array
*/
private function get_type_options() {
global $wpdb, $updraftcentral_host_plugin, $updraftcentral_main;
$options = array();
if (!function_exists('get_post_mime_types')) {
// For a much later version of WP the "get_post_mime_types" is located
// in a different folder. So, we make sure that we have it loaded before
// actually using it.
if (version_compare($updraftcentral_main->get_wordpress_version(), '3.5', '>=')) {
require_once(ABSPATH.WPINC.'/post.php');
} else {
// For WP 3.4, the "get_post_mime_types" is located in the location provided below.
require_once(ABSPATH.'wp-admin/includes/post.php');
}
}
$post_mime_types = get_post_mime_types();
$type_options = $wpdb->get_col("SELECT `post_mime_type` FROM {$wpdb->posts} WHERE `post_type` = 'attachment' AND `post_status` = 'inherit' GROUP BY `post_mime_type` ORDER BY `post_mime_type` DESC");
foreach ($post_mime_types as $mime_type => $label) {
if (!wp_match_mime_types($mime_type, $type_options)) continue;
$options[] = array('label' => $label[0], 'value' => esc_attr($mime_type));
}
$options[] = array('label' => $updraftcentral_host_plugin->retrieve_show_message('unattached'), 'value' => 'detached');
return $options;
}
/**
* Retrieves media items that haven't been attached to any posts
*
* @return array
*/
private function get_unattached_ids() {
global $wpdb;
return $wpdb->get_col("SELECT `ID` FROM {$wpdb->posts} WHERE `post_type` = 'attachment' AND `post_status` = 'inherit' AND `post_parent` = '0'");
}
/**
* Retrieves IDs of media items that has the given mime type
*
* @param string $type The mime type to search for
* @return array
*/
private function get_type_ids($type) {
global $wpdb;
return $wpdb->get_col($wpdb->prepare("SELECT `ID` FROM {$wpdb->posts} WHERE `post_type` = 'attachment' AND `post_status` = 'inherit' AND `post_mime_type` LIKE %s", $type.'/%'));
}
/**
* Checks whether we have the required fields submitted and the user has
* the capabilities to execute the requested action
*
* @param array $capabilities The capabilities to check and validate
*
* @return array|void
*/
private function _validate_capabilities($capabilities) {
foreach ($capabilities as $capability) {
if (!current_user_can($capability)) {
return $this->_generic_error_response('insufficient_permission');
}
}
}
/**
* Populates the $_REQUEST global variable with the submitted data
*
* @param array $params Submitted data received from UpdraftCentral
* @return array
*/
private function populate_request($params) {
if (!empty($params)) {
foreach ($params as $key => $value) {
$_REQUEST[$key] = $value;
}
}
}
/**
* Handles image editing requests coming from UpdraftCentral
*
* @param array $params Containing all the needed information to filter the results of the current request
* @return array
*/
public function image_editor($params) {
$error = $this->_validate_capabilities(array('edit_posts'));
if (!empty($error)) return $error;
$attachment_id = (int) $params['postid'];
$this->populate_request($params);
if (!function_exists('load_image_to_edit')) {
require_once(ABSPATH.'wp-admin/includes/image.php');
}
include_once(ABSPATH.'wp-admin/includes/image-edit.php');
$msg = false;
switch ($params['do']) {
case 'save':
case 'scale':
$msg = wp_save_image($attachment_id);
break;
case 'restore':
$msg = wp_restore_image($attachment_id);
break;
}
$msg = (false !== $msg) ? json_encode($msg) : $msg;
return $this->_response(array('content' => $msg));
}
/**
* Handles image preview requests coming from UpdraftCentral
*
* @param array $params Containing all the needed information to filter the results of the current request
* @return array
*/
public function image_preview($params) {
$error = $this->_validate_capabilities(array('edit_posts'));
if (!empty($error)) return $error;
if (!function_exists('load_image_to_edit')) {
require_once(ABSPATH.'wp-admin/includes/image.php');
}
include_once(ABSPATH.'wp-admin/includes/image-edit.php');
$this->populate_request($params);
$post_id = (int) $params['postid'];
ob_start();
stream_preview_image($post_id);
$content = ob_get_contents();
ob_end_clean();
return $this->_response(array('content' => base64_encode($content)));
}
}

View File

@@ -0,0 +1,15 @@
<?php
if (!defined('UPDRAFTCENTRAL_CLIENT_DIR')) die('No access.');
// Load the posts command class since we're going to be extending it for our page module service/command
// class in order to minimize redundant shareable methods.
if (!class_exists('UpdraftCentral_Posts_Commands')) require_once('posts.php');
/**
* Handles Pages Commands
*/
class UpdraftCentral_Pages_Commands extends UpdraftCentral_Posts_Commands {
protected $post_type = 'page';
}

View File

@@ -0,0 +1,700 @@
<?php
if (!defined('UPDRAFTCENTRAL_CLIENT_DIR')) die('No access.');
/**
* Handles UpdraftCentral Plugin Commands which basically handles
* the installation and activation of a plugin
*/
class UpdraftCentral_Plugin_Commands extends UpdraftCentral_Commands {
private $switched = false;
/**
* Function that gets called before every action
*
* @param string $command a string that corresponds to UDC command to call a certain method for this class.
* @param array $data an array of data post or get fields
* @param array $extra_info extrainfo use in the udrpc_action, e.g. user_id
*
* link to udrpc_action main function in class UpdraftCentral_Listener
*/
public function _pre_action($command, $data, $extra_info) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- This function is called from listener.php and $extra_info is being sent.
// Here we assign the current blog_id to a variable $blog_id
$blog_id = get_current_blog_id();
if (!empty($data['site_id'])) $blog_id = $data['site_id'];
if (function_exists('switch_to_blog') && is_multisite() && $blog_id) {
$this->switched = switch_to_blog($blog_id);
}
}
/**
* Function that gets called after every action
*
* @param string $command a string that corresponds to UDC command to call a certain method for this class.
* @param array $data an array of data post or get fields
* @param array $extra_info extrainfo use in the udrpc_action, e.g. user_id
*
* link to udrpc_action main function in class UpdraftCentral_Listener
*/
public function _post_action($command, $data, $extra_info) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Unused parameter is present because the caller from UpdraftCentral_Listener class uses 3 arguments.
// Here, we're restoring to the current (default) blog before we switched
if ($this->switched) restore_current_blog();
}
/**
* Constructor
*/
public function __construct() {
$this->_admin_include('plugin.php', 'file.php', 'template.php', 'class-wp-upgrader.php', 'plugin-install.php', 'update.php');
}
/**
* Installs and activates a plugin through upload
*
* @param array $params Parameter array containing information pertaining the currently uploaded plugin
* @return array Contains the result of the current process
*/
public function upload_plugin($params) {
return $this->process_chunk_upload($params, 'plugin');
}
/**
* Checks whether the plugin is currently installed and activated.
*
* @param array $query Parameter array containing the name of the plugin to check
* @return array Contains the result of the current process
*/
public function is_plugin_installed($query) {
if (!isset($query['plugin']))
return $this->_generic_error_response('plugin_name_required');
$result = $this->_get_plugin_info($query);
return $this->_response($result);
}
/**
* Applies currently requested action for plugin processing
*
* @param string $action The action to apply (e.g. activate or install)
* @param array $query Parameter array containing information for the currently requested action
*
* @return array
*/
private function _apply_plugin_action($action, $query) {
$result = array();
switch ($action) {
case 'activate':
case 'network_activate':
$info = $this->_get_plugin_info($query);
if ($info['installed']) {
$activate = activate_plugin($info['plugin_path']);
if (is_wp_error($activate)) {
$result = $this->_generic_error_response('generic_response_error', array(
'plugin' => $query['plugin'],
'error_code' => 'generic_response_error',
'error_message' => $activate->get_error_message(),
'info' => $this->_get_plugin_info($query)
));
} else {
$result = array('activated' => true, 'info' => $this->_get_plugin_info($query), 'last_state' => $info);
}
} else {
$result = $this->_generic_error_response('plugin_not_installed', array(
'plugin' => $query['plugin'],
'error_code' => 'plugin_not_installed',
'error_message' => __('The plugin you wish to activate is either not installed or has been removed recently.', 'updraftplus'),
'info' => $info
));
}
break;
case 'deactivate':
case 'network_deactivate':
$info = $this->_get_plugin_info($query);
if ($info['active']) {
deactivate_plugins($info['plugin_path']);
if (!is_plugin_active($info['plugin_path'])) {
$result = array('deactivated' => true, 'info' => $this->_get_plugin_info($query), 'last_state' => $info);
} else {
$result = $this->_generic_error_response('deactivate_plugin_failed', array(
'plugin' => $query['plugin'],
'error_code' => 'deactivate_plugin_failed',
'error_message' => __('There appears to be a problem deactivating the intended plugin.', 'updraftplus').' '.__('Please check your permissions and try again.', 'updraftplus'),
'info' => $this->_get_plugin_info($query)
));
}
} else {
$result = $this->_generic_error_response('not_active', array(
'plugin' => $query['plugin'],
'error_code' => 'not_active',
'error_message' => __('The plugin you wish to deactivate is currently not active or is already deactivated.', 'updraftplus'),
'info' => $info
));
}
break;
case 'install':
$api = plugins_api('plugin_information', array(
'slug' => $query['slug'],
'fields' => array(
'short_description' => false,
'sections' => false,
'requires' => false,
'rating' => false,
'ratings' => false,
'downloaded' => false,
'last_updated' => false,
'added' => false,
'tags' => false,
'compatibility' => false,
'homepage' => false,
'donate_link' => false,
)
));
$info = $this->_get_plugin_info($query);
if (is_wp_error($api)) {
$result = $this->_generic_error_response('generic_response_error', array(
'plugin' => $query['plugin'],
'error_code' => 'generic_response_error',
'error_message' => $api->get_error_message(),
'info' => $info
));
} else {
$installed = $info['installed'];
$error_code = $error_message = '';
if (!$installed) {
// WP < 3.7
if (!class_exists('Automatic_Upgrader_Skin')) include_once(dirname(dirname(__FILE__)).'/classes/class-automatic-upgrader-skin.php');
$skin = new Automatic_Upgrader_Skin();
$upgrader = new Plugin_Upgrader($skin);
$download_link = $api->download_link;
$installed = $upgrader->install($download_link);
if (is_wp_error($installed)) {
$error_code = $installed->get_error_code();
$error_message = $installed->get_error_message();
} elseif (is_wp_error($skin->result)) {
$error_code = $skin->result->get_error_code();
$error_message = $skin->result->get_error_message();
$error_data = $skin->result->get_error_data($error_code);
if (!empty($error_data)) {
if (is_array($error_data)) $error_data = json_encode($error_data);
$error_message .= ' '.$error_data;
}
} elseif (is_null($installed) || !$installed) {
global $wp_filesystem;
$upgrade_messages = $skin->get_upgrade_messages();
if (!class_exists('WP_Filesystem_Base')) include_once(ABSPATH.'/wp-admin/includes/class-wp-filesystem-base.php');
// Pass through the error from WP_Filesystem if one was raised.
if ($wp_filesystem instanceof WP_Filesystem_Base && is_wp_error($wp_filesystem->errors) && $wp_filesystem->errors->get_error_code()) {
$error_code = $wp_filesystem->errors->get_error_code();
$error_message = $wp_filesystem->errors->get_error_message();
} elseif (!empty($upgrade_messages)) {
// We're only after for the last feedback that we received from the install process. Mostly,
// that is where the last error has been inserted.
$messages = $skin->get_upgrade_messages();
$error_code = 'install_failed';
$error_message = end($messages);
} else {
$error_code = 'unable_to_connect_to_filesystem';
$error_message = __('Unable to connect to the filesystem.', 'updraftplus').' '.__('Please confirm your credentials.', 'updraftplus');
}
}
}
if (!$installed || is_wp_error($installed)) {
$result = $this->_generic_error_response('plugin_install_failed', array(
'plugin' => $query['plugin'],
'error_code' => $error_code,
'error_message' => $error_message,
'info' => $this->_get_plugin_info($query)
));
} else {
$result = array('installed' => true, 'info' => $this->_get_plugin_info($query), 'last_state' => $info);
}
}
break;
}
return $result;
}
/**
* Preloads the submitted credentials to the global $_POST variable
*
* @param array $query Parameter array containing information for the currently requested action
*/
private function _preload_credentials($query) {
if (!empty($query) && isset($query['filesystem_credentials'])) {
parse_str($query['filesystem_credentials'], $filesystem_credentials);
if (is_array($filesystem_credentials)) {
foreach ($filesystem_credentials as $key => $value) {
// Put them into $_POST, which is where request_filesystem_credentials() checks for them.
$_POST[$key] = $value;
}
}
}
}
/**
* Checks whether we have the required fields submitted and the user has
* the capabilities to execute the requested action
*
* @param array $query The submitted information
* @param array $fields The required fields to check
* @param array $capabilities The capabilities to check and validate
*
* @return array|string
*/
private function _validate_fields_and_capabilities($query, $fields, $capabilities) {
$error = '';
if (!empty($fields)) {
for ($i=0; $i<count($fields); $i++) {
$field = $fields[$i];
if (!isset($query[$field])) {
if ('keyword' === $field) {
$error = $this->_generic_error_response('keyword_required');
} else {
$error = $this->_generic_error_response('plugin_'.$query[$field].'_required');
}
break;
}
}
}
if (empty($error) && !empty($capabilities)) {
for ($i=0; $i<count($capabilities); $i++) {
if (!current_user_can($capabilities[$i])) {
$error = $this->_generic_error_response('plugin_insufficient_permission');
break;
}
}
}
return $error;
}
/**
* Processing an action for multiple items
*
* @param array $query Parameter array containing a list of plugins to process
* @return array Contains the results of the bulk process
*/
public function process_action_in_bulk($query) {
$action = isset($query['action']) ? $query['action'] : '';
$items = isset($query['args']) ? $query['args']['items'] : array();
$results = array();
if (!empty($action) && !empty($items) && is_array($items)) {
foreach ($items as $value) {
if (method_exists($this, $action)) {
$results[] = $this->$action($value);
}
}
}
return $this->_response($results);
}
/**
* Activates the plugin
*
* @param array $query Parameter array containing the name of the plugin to activate
* @return array Contains the result of the current process
*/
public function activate_plugin($query) {
$fields = array('plugin');
$permissions = array('activate_plugins');
$error = $this->_validate_fields_and_capabilities($query, $fields, $permissions);
if (!empty($error)) {
return $error;
}
$this->_preload_credentials($query);
$result = $this->_apply_plugin_action((!empty($query['multisite']) && (bool) $query['multisite']) ? 'network_activate' : 'activate', $query);
if (empty($result['activated'])) {
return $result;
}
return $this->_response($result);
}
/**
* Deactivates the plugin
*
* @param array $query Parameter array containing the name of the plugin to deactivate
* @return array Contains the result of the current process
*/
public function deactivate_plugin($query) {
$fields = array('plugin');
$permissions = array('activate_plugins');
$error = $this->_validate_fields_and_capabilities($query, $fields, $permissions);
if (!empty($error)) {
return $error;
}
$this->_preload_credentials($query);
$result = $this->_apply_plugin_action((!empty($query['multisite']) && (bool) $query['multisite']) ? 'network_deactivate' : 'deactivate', $query);
if (empty($result['deactivated'])) {
return $result;
}
return $this->_response($result);
}
/**
* Download, install and activates the plugin
*
* @param array $query Parameter array containing the filesystem credentials entered by the user along with the plugin name and slug
* @return array Contains the result of the current process
*/
public function install_activate_plugin($query) {
$fields = array('plugin', 'slug');
$permissions = array('install_plugins', 'activate_plugins');
$error = $this->_validate_fields_and_capabilities($query, $fields, $permissions);
if (!empty($error)) {
return $error;
}
$this->_preload_credentials($query);
$result = $this->_apply_plugin_action('install', $query);
if (!empty($result['installed']) && $result['installed']) {
$result = $this->_apply_plugin_action((!empty($query['multisite']) && (bool) $query['multisite']) ? 'network_activate' : 'activate', $query);
if (empty($result['activated'])) {
return $result;
}
} else {
return $result;
}
return $this->_response($result);
}
/**
* Download, install the plugin
*
* @param array $query Parameter array containing the filesystem credentials entered by the user along with the plugin name and slug
* @return array Contains the result of the current process
*/
public function install_plugin($query) {
$fields = array('plugin', 'slug');
$permissions = array('install_plugins');
$error = $this->_validate_fields_and_capabilities($query, $fields, $permissions);
if (!empty($error)) {
return $error;
}
$this->_preload_credentials($query);
$result = $this->_apply_plugin_action('install', $query);
if (empty($result['installed'])) {
return $result;
}
return $this->_response($result);
}
/**
* Uninstall/delete the plugin
*
* @param array $query Parameter array containing the filesystem credentials entered by the user along with the plugin name and slug
* @return array Contains the result of the current process
*/
public function delete_plugin($query) {
$fields = array('plugin');
$permissions = array('delete_plugins');
$error = $this->_validate_fields_and_capabilities($query, $fields, $permissions);
if (!empty($error)) {
return $error;
}
$this->_preload_credentials($query);
$info = $this->_get_plugin_info($query);
if ($info['installed']) {
// Deactivate first before delete to invalidate the activate
// state/status prior to deleting the item. Otherwise, WordPress will keep
// that state, and as soon as you install the same plugin it will be automatically
// activated since it's previous state was kept.
deactivate_plugins($info['plugin_path']);
$deleted = delete_plugins(array($info['plugin_path']));
if ($deleted) {
$result = array('deleted' => true, 'info' => $this->_get_plugin_info($query), 'last_state' => $info);
} else {
return $this->_generic_error_response('delete_plugin_failed', array(
'plugin' => $query['plugin'],
'error_code' => 'delete_plugin_failed',
'info' => $info
));
}
} else {
return $this->_generic_error_response('plugin_not_installed', array(
'plugin' => $query['plugin'],
'error_code' => 'plugin_not_installed',
'info' => $info
));
}
return $this->_response($result);
}
/**
* Updates/upgrade the plugin
*
* @param array $query Parameter array containing the filesystem credentials entered by the user along with the plugin name and slug
* @return array Contains the result of the current process
*/
public function update_plugin($query) {
$fields = array('plugin', 'slug');
$permissions = array('update_plugins');
$error = $this->_validate_fields_and_capabilities($query, $fields, $permissions);
if (!empty($error)) {
return $error;
}
$this->_preload_credentials($query);
// Make sure that we still have the plugin installed before running
// the update process
$info = $this->_get_plugin_info($query);
if ($info['installed']) {
// Load the updates command class if not existed
if (!class_exists('UpdraftCentral_Updates_Commands')) include_once('updates.php');
$update_command = new UpdraftCentral_Updates_Commands($this->rc);
$result = $update_command->update_plugin($info['plugin_path'], $query['slug']);
if (!empty($result['error'])) {
$result['values'] = array('plugin' => $query['plugin'], 'info' => $info);
}
} else {
return $this->_generic_error_response('plugin_not_installed', array(
'plugin' => $query['plugin'],
'error_code' => 'plugin_not_installed',
'info' => $info
));
}
return $this->_response($result);
}
/**
* Gets the plugin information along with its active and install status
*
* @internal
* @param array $query Contains either the plugin name or slug or both to be used when retrieving information
* @return array
*/
private function _get_plugin_info($query) {
$info = array(
'active' => false,
'installed' => false
);
// Clear plugin cache so that newly installed/downloaded plugins
// gets reflected when calling "get_plugins"
if (function_exists('wp_clean_plugins_cache')) {
wp_clean_plugins_cache();
}
// Gets all plugins available.
$get_plugins = get_plugins();
// Loops around each plugin available.
foreach ($get_plugins as $key => $value) {
$slug = $this->extract_slug_from_info($key, $value);
// If the plugin name matches that of the specified name, it will gather details.
// In case name check isn't enough, we'll use slug to verify if the plugin being queried is actually installed.
//
// Reason for name check failure:
// Due to plugin name inconsistencies - where wordpress.org registered plugin name is different
// from the actual plugin files's metadata (found inside the plugin's PHP file itself).
if ((!empty($query['plugin']) && html_entity_decode($value['Name']) === html_entity_decode($query['plugin'])) || (!empty($query['slug']) && $slug === $query['slug'])) {
$info['installed'] = true;
$info['active'] = is_plugin_active($key);
$info['plugin_path'] = $key;
$info['data'] = $value;
$info['name'] = $value['Name'];
$info['slug'] = $slug;
break;
}
}
return $info;
}
/**
* Extract the slug from the plugin data
*
* @param string $key They key of the current info
* @param array $info Data pulled from the plugin file
*
* @return string
*/
private function extract_slug_from_info($key, $info) {
if (!is_array($info) || empty($info) || empty($key)) return '';
$temp = explode('/', $key);
// With WP standards textdomain must always be equal to the plugin's folder name
// but for premium plugins this may not always be the case thus, we extract the folder
// name from the key as the default slug.
$slug = basename($temp[0], '.php');
if (!empty($info['TextDomain']) && 1 === count($temp)) {
// For plugin without folder we compare the extracted slug with the 'TextDomain'
// and if they're not equal then 'TextDomain' will assume as slug.
if ($slug != $info['TextDomain']) $slug = $info['TextDomain'];
}
// If in case the user kept the hello-dolly plugin then we'll make sure that it gets
// the proper slug for it, otherwise, we'll end up with the wrong slug 'hello' instead of
// 'hello-dolly'. Wrong slug will produce error in UpdraftCentral when running it against
// wordpress.org for further information retrieval.
$slug = ('Hello Dolly' === $info['Name']) ? 'hello-dolly' : $slug;
return $slug;
}
/**
* Loads all available plugins with additional attributes and settings needed by UpdraftCentral
*
* @param array $query Parameter array Any available parameters needed for this action
* @return array Contains the result of the current process
*/
public function load_plugins($query) {
$permissions = array('install_plugins', 'activate_plugins');
if (is_multisite() && !is_super_admin(get_current_user_id())) $permissions = array('activate_plugins');
$error = $this->_validate_fields_and_capabilities($query, array(), $permissions);
if (!empty($error)) {
return $error;
}
$website = get_bloginfo('name');
$results = array();
// Load the updates command class if not existed
if (!class_exists('UpdraftCentral_Updates_Commands')) include_once('updates.php');
$updates = new UpdraftCentral_Updates_Commands($this->rc);
// Get plugins for update
$plugin_updates = $updates->get_item_updates('plugins');
// Get all plugins
$plugins = get_plugins();
if (is_multisite() && !is_super_admin(get_current_user_id())) {
// If the "Plugins" menu is disabled for the subsites on a multisite
// network then we return an empty "plugins" array.
$menu_items = get_site_option('menu_items');
if (empty($menu_items) || !isset($menu_items['plugins'])) {
$plugins = array();
} else {
$show_network_active = apply_filters('show_network_active_plugins', current_user_can('manage_network_plugins'));
$filtered_plugins = array();
foreach ($plugins as $file => $data) {
if (is_network_only_plugin($file) && !is_plugin_active($file)) {
if ($show_network_active) $filtered_plugins[$file] = $data;
} elseif (is_plugin_active_for_network($file)) {
if ($show_network_active) $filtered_plugins[$file] = $data;
} else {
$filtered_plugins[$file] = $data;
}
}
$plugins = $filtered_plugins;
}
}
foreach ($plugins as $key => $value) {
$slug = $this->extract_slug_from_info($key, $value);
$plugin = new stdClass();
$plugin->name = $value['Name'];
$plugin->description = $value['Description'];
$plugin->slug = $slug;
$plugin->version = $value['Version'];
$plugin->author = $value['Author'];
$plugin->status = is_plugin_active($key) ? 'active' : 'inactive';
$plugin->website = $website;
$plugin->multisite = is_multisite();
$plugin->site_url = trailingslashit(get_bloginfo('url'));
if (!empty($plugin_updates[$key])) {
$update_info = $plugin_updates[$key];
if (version_compare($update_info->Version, $update_info->update->new_version, '<')) {
if (!empty($update_info->update->new_version)) $plugin->latest_version = $update_info->update->new_version;
if (!empty($update_info->update->package)) $plugin->download_link = $update_info->update->package;
if (!empty($update_info->update->sections)) $plugin->sections = $update_info->update->sections;
}
}
if (empty($plugin->short_description) && !empty($plugin->description)) {
// Only pull the first sentence as short description, it should be enough rather than displaying
// an empty description or a full blown one which the user can access anytime if they press on
// the view details link in UpdraftCentral.
$temp = explode('.', $plugin->description);
$short_description = $temp[0];
// Adding the second sentence wouldn't hurt, in case the first sentence is too short.
if (isset($temp[1])) $short_description .= '.'.$temp[1];
$plugin->short_description = $short_description.'.';
}
$results[] = $plugin;
}
$result = array(
'plugins' => $results,
'is_super_admin' => is_super_admin(),
);
$result = array_merge($result, $this->_get_backup_credentials_settings(WP_PLUGIN_DIR));
return $this->_response($result);
}
/**
* Gets the backup and security credentials settings for this website
*
* @param array $query Parameter array Any available parameters needed for this action
* @return array Contains the result of the current process
*/
public function get_plugin_requirements() {
return $this->_response($this->_get_backup_credentials_settings(WP_PLUGIN_DIR));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,391 @@
<?php
if (!defined('UPDRAFTCENTRAL_CLIENT_DIR')) die('No access.');
/**
* - A container for RPC commands (white label reporting UpdraftCentral commands). Commands map exactly onto method names (and hence this class should not implement anything else, beyond the constructor, and private methods)
* - Return format is array('response' => (string - a code), 'data' => (mixed));
*
* RPC commands are not allowed to begin with an underscore. So, any private methods can be prefixed with an underscore.
*/
class UpdraftCentral_Reporting_Commands extends UpdraftCentral_Commands {
private $valid_statuses = array(
'recurring',
'manual',
);
private $max_number_of_recipients = 100;
/**
* Update existing reports.
*
* @param array $reports Reports data which will be saved.
*
* @return void
*/
private function _update_existing_reports($reports) {
update_option('updraftcentral_reporting_reports', $reports);
}
/**
* Normalize email address - lowercase and sanitize.
*
* @param string $email Email address to normalize.
*
* @return string Normalized email.
*/
private function _normalize_email($email) {
return strtolower(sanitize_email($email));
}
/**
* Get existing reports.
*
* @return array List of all reports.
*/
private function _get_existing_reports() {
return (array) get_option('updraftcentral_reporting_reports', array());
}
/**
* Update existing sent reports.
*
* @param array $sent_reports Sent reports data which will be saved.
*
* @return void
*/
private function _update_existing_sent_reports($sent_reports) {
update_option('updraftcentral_reporting_sent_reports', $sent_reports);
}
/**
* Get existing sent reports.
*
* @return array List of all sent reports.
*/
private function _get_existing_sent_reports() {
return (array) get_option('updraftcentral_reporting_sent_reports', array());
}
/**
* Get all the reports (scheduled or not scheduled).
*
* @param array $data Data array containing report IDs to fetch.
*
* @return array An array of reports.
*/
public function get_reports($data) {
$all_reports = $this->_get_existing_reports();
$sent_reports = array_reverse($this->_get_existing_sent_reports());
foreach ($sent_reports as $key => $sent_report) {
$sent_report['download_url'] = '';
if (!empty($sent_report['pdf_attachment_id'])) {
$sent_report['download_url'] = wp_get_attachment_url(absint($sent_report['pdf_attachment_id']));
}
$sent_reports[$key] = $sent_report;
}
if (empty($data['report_ids']) || !is_array($data['report_ids'])) {
return $this->_response(array(
'reports' => $all_reports,
'sent_reports' => $sent_reports,
));
}
$report_ids = array();
foreach ($data['report_ids'] as $report_id) {
$report_ids[] = sanitize_key(strval($report_id));
}
return $this->_response((array(
'reports' => array_intersect_key($all_reports, array_flip($report_ids)),
'sent_reports' => $sent_reports,
)));
}
/**
* Delete a report.
*
* @param array $data Data array containing report ID to delete.
*
* @return array An array containing the result of the current process
*/
public function delete_report($data) {
// Permission check.
if (!current_user_can('manage_options')) {
$result = array("error" => true, "message" => "not_allowed");
return $this->_response($result);
}
// Return early if valid data structure is not present.
if (!is_array($data)) {
$result = array("error" => true, "message" => "invalid_data");
return $this->_response($result);
}
$report_id = empty($data['id']) ? '' : sanitize_key(strval($data['id']));
// Return early if ID not supplied.
if (empty($report_id)) {
$result = array('error' => true, 'message' => 'missing_id');
return $this->_response($result);
}
// Get the reports from options table.
$reports = $this->_get_existing_reports();
// Check if the ID to delete is present.
if (!isset($reports[$report_id])) {
$result = array('error' => true, 'message' => 'invalid_id');
return $this->_response($result);
}
unset($reports[$report_id]);
$this->_update_existing_reports($reports);
$result = array("error" => false, "message" => "reports_updated");
return $this->_response($result);
}
/**
* Add a new report.
*
* @param array $data Report information to add
*
* @return array An array containing the result of the current process
*/
public function add_report($data) {
// Permission check.
if (!current_user_can('manage_options')) {
$result = array("error" => true, "message" => "not_allowed");
return $this->_response($result);
}
// Return early if valid report structure is not present.
if (!is_array($data)) {
$result = array("error" => true, "message" => "invalid_report_data");
return $this->_response($result);
}
$report_name = isset($data['name']) ? sanitize_text_field(strval($data['name'])) : '';
if ('' === $report_name) {
$result = array("error" => true, "message" => "empty_name");
return $this->_response($result);
}
$report_status = empty($data['status']) ? '' : sanitize_text_field(strval($data['status']));
if (!in_array($report_status, $this->valid_statuses)) {
$result = array("error" => true, "message" => "status_invalid");
return $this->_response($result);
}
$template_id = empty($data['template_id']) ? '' : sanitize_key($data['template_id']);
if (empty($template_id)) {
$result = array("error" => true, "message" => "template_id_invalid");
return $this->_response($result);
}
$recipients = (isset($data['recipients']) && is_array($data['recipients'])) ? $data['recipients'] : array();
if (count($recipients) > $this->max_number_of_recipients) {
$result = array("error" => true, "message" => "max_number_of_recipients_exceeded");
return $this->_response($result);
}
$recipients = array_unique(array_map(array($this, '_normalize_email'), $recipients));
// Sanity check for invalid email.
foreach ($recipients as $email) {
if (!is_email($email)) {
$result = array(
"error" => true,
"message" => "recipients_invalid",
"values" => array(
'invalid_email' => $email,
)
);
return $this->_response($result);
}
}
if (empty($recipients)) {
$result = array("error" => true, "message" => "no_recipients_provided");
return $this->_response($result);
}
// Get existing reports from options table.
$existing_reports = $this->_get_existing_reports();
// Report.
$report = array();
$report_id = empty($data['id']) ? '' : sanitize_key(strval($data['id']));
if (empty($report_id)) {
// First report timestamp and formatted date.
$next_report_timestamp = strtotime("+1 month", time());
$next_report_formatted_date = date("j M, g:i a", $next_report_timestamp);
$report_id = UpdraftPlus_Manipulation_Functions::generate_random_string(10);
$report_id_generation_loops = 0;
// Sanity check to check if the report ID exists.
while (isset($existing_reports[$report_id])) {
$report_id = UpdraftPlus_Manipulation_Functions::generate_random_string(10);
++$report_id_generation_loops;
// If we somehow exceed the max generation loops then return error - which will not happen in almost any case.
if ($report_id_generation_loops > 10) {
$result = array("error" => true, "message" => "report_id_generation_failed");
return $this->_response($result);
}
}
$report = array(
'id' => $report_id,
'name' => $report_name,
'status' => $report_status,
'template_id' => $template_id,
'recipients' => $recipients,
'last_report_timestamp' => 0,
'last_report_formatted_date' => __('N/A', 'updraftplus'),
'next_report_timestamp' => $next_report_timestamp,
'next_report_formatted_date' => $next_report_formatted_date,
);
} elseif (!empty($existing_reports[$report_id])) {
$report = $existing_reports[$report_id];
$report['name'] = $report_name;
$report['status'] = $report_status;
$report['template_id'] = $template_id;
$report['recipients'] = $recipients;
} else {
$result = array("error" => true, "message" => "report_does_not_exist");
return $this->_response($result);
}
// Add the new report.
$existing_reports[$report_id] = $report;
// Update the reports.
$this->_update_existing_reports($existing_reports);
$result = array(
"error" => false,
"message" => "reports_updated",
"values" => array(
'report' => $report,
)
);
return $this->_response($result);
}
/**
* Add a sent report.
*
* @param array $data Report information to add
*
* @return array An array containing the result of the current process
*/
public function add_sent_reports($data) {
// Permission check.
if (!current_user_can('manage_options')) {
$result = array("error" => true, "message" => "not_allowed");
return $this->_response($result);
}
// Return early if valid report structure is not present.
if (!is_array($data) || empty($data['sent_reports_data'])) {
$result = array("error" => true, "message" => "invalid_sent_report_data");
return $this->_response($result);
}
$reports = $this->_get_existing_reports();
$sent_reports = $this->_get_existing_sent_reports();
$return_data = array();
// Loop through all the sent reports.
foreach ($data['sent_reports_data'] as $report_data) {
$report_id = sanitize_key(strval($report_data['report_id']));
// Skip if no report of this ID exists.
if (empty($reports[$report_id])) {
continue;
}
$report_sent_at_timestamp = time();
$report_sent_at_formatted_date = date("j M, g:i a", $report_sent_at_timestamp);
// Change the last report time of the report.
$reports[$report_id]['last_report_timestamp'] = $report_sent_at_timestamp;
$reports[$report_id]['last_report_formatted_date'] = $report_sent_at_formatted_date;
// Save the PDF as attachment.
$pdf_attachment_id = 0;
$uploads_dir = wp_upload_dir();
$custom_upload_directory = trailingslashit($uploads_dir['basedir']) . 'updraftcentral-white-label-reporting-pdfs/';
$custom_upload_url = trailingslashit($uploads_dir['baseurl']) . 'updraftcentral-white-label-reporting-pdfs/';
$filename = sanitize_text_field($reports[$report_id]['name']) . '-' . $report_sent_at_timestamp . '-' . UpdraftPlus_Manipulation_Functions::generate_random_string(5) . '.pdf';
$full_path = $custom_upload_directory . basename($filename);
// Sanity check to test directory exists.
wp_mkdir_p(dirname($full_path));
file_put_contents($full_path, base64_decode($report_data['pdf_content']));
$wp_filetype = wp_check_filetype($filename, null);
$attachment = array(
'guid' => $custom_upload_url . basename($filename),
'post_mime_type' => $wp_filetype['type'],
'post_title' => sanitize_file_name(pathinfo($filename, PATHINFO_FILENAME)),
'post_content' => '',
'post_status' => 'inherit',
);
$pdf_attachment_id = wp_insert_attachment($attachment, $full_path);
require_once(ABSPATH . 'wp-admin/includes/image.php');
$attach_data = wp_generate_attachment_metadata($pdf_attachment_id, $full_path);
wp_update_attachment_metadata($pdf_attachment_id, $attach_data);
$new_sent_report = array(
'report' => $reports[$report_id]['name'],
'sent' => (bool) $report_data['sent'],
'template_id_used' => sanitize_key($report_data['template_id']),
'template_name_used' => sanitize_text_field($report_data['template_name']),
'services' => array_map('sanitize_text_field', $report_data['services']),
'sent_at' => $report_sent_at_formatted_date,
'number_of_recipients' => absint($report_data['number_of_recipients']),
'pdf_attachment_id' => $pdf_attachment_id,
);
$sent_reports[] = $new_sent_report;
$return_data[] = $new_sent_report;
}
$this->_update_existing_reports($reports);
$this->_update_existing_sent_reports($sent_reports);
$result = array(
"error" => false,
"message" => "sent_reports_updated",
"data" => $return_data,
);
return $this->_response($result);
}
}

View File

@@ -0,0 +1,92 @@
<?php
if (!defined('UPDRAFTCENTRAL_CLIENT_DIR')) die('No access.');
/**
* Handles commands to access the REST API.
*
* This action is used to relay any REST API request through UDC command.
* From UDC we don't have direct authentication to child site's REST APIs, therefore this middleware.
*
* UDC authentication logic automatically authenticates the user with which you have added the site to UDC dashboard.
* And then the request is just relayed to the WordPress rest api handler, which takes care of permission callback and everything as usual.
*/
class UpdraftCentral_REST_API_Access_Commands extends UpdraftCentral_Commands {
protected $switched = false;
/**
* Function that gets called before every action
*
* Link to udrpc_action main function in class UpdraftCentral_Listener
*
* @param string $command a string that corresponds to UDC command to call a certain method for this class.
* @param array $data an array of data post or get fields
*/
public function _pre_action($command, $data) {
$blog_id = get_current_blog_id();
if (!empty($data['site_id'])) $blog_id = $data['site_id'];
if (function_exists('switch_to_blog') && is_multisite() && $blog_id) {
$this->switched = switch_to_blog($blog_id);
}
}
/**
* Function that gets called after every action
*
* Link to udrpc_action main function in class UpdraftCentral_Listener
*/
public function _post_action() {
// Here, we're restoring to the current (default) blog before we switched
if ($this->switched) restore_current_blog();
}
/**
* Relays the REST API request.
*
* @param array $params The parameters for the request.
*
* @return array The response from the REST API, wrapped in udrpc response structure.
*/
public function handle_request($params) {
$route = untrailingslashit(!empty($params['route']) ? $params['route'] : '');
$method = !empty($params['method']) ? $params['method'] : 'GET';
$body = !empty($params['body']) ? $params['body'] : null;
// Return early if the route is empty.
if (empty($route)) {
return $this->_generic_error_response('route_empty', array(
'prefix' => 'updraftcentral',
'command' => 'handle_request',
'class' => 'UpdraftCentral_REST_API_Access_Commands'
));
}
if (!class_exists('WP_REST_Request')) {
return $this->_generic_error_response('rest_api_not_available_on_this_wordpress_version', array(
'prefix' => 'updraftcentral',
'command' => 'handle_request',
'class' => 'UpdraftCentral_REST_API_Access_Commands'
));
}
$request = new WP_REST_Request($method, '/' . $route);
if (!empty($body)) {
$request->set_body(json_encode($body));
$request->set_header('Content-Type', 'application/json');
}
// Do the request.
$response = rest_do_request($request);
// Return if error.
if (true === $response->is_error()) {
return $this->_generic_error_response('rest_request_failed', $response->as_error());
}
// `get_data` should always return JSON-serializable data.
return $this->_response(array('rest_data' => $response->get_data(), 'headers' => $response->headers));
}
}

View File

@@ -0,0 +1,964 @@
<?php
if (!defined('UPDRAFTCENTRAL_CLIENT_DIR')) die('No access.');
/**
* Handles UpdraftCentral Theme Commands which basically handles
* the installation and activation of a theme
*/
class UpdraftCentral_Theme_Commands extends UpdraftCentral_Commands {
private $switched = false;
/**
* Function that gets called before every action
*
* @param string $command a string that corresponds to UDC command to call a certain method for this class.
* @param array $data an array of data post or get fields
* @param array $extra_info extrainfo use in the udrpc_action, e.g. user_id
*
* link to udrpc_action main function in class UpdraftCentral_Listener
*/
public function _pre_action($command, $data, $extra_info) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- This function is called from listener.php and $extra_info is being sent.
// Here we assign the current blog_id to a variable $blog_id
$blog_id = get_current_blog_id();
if (!empty($data['site_id'])) $blog_id = $data['site_id'];
if (function_exists('switch_to_blog') && is_multisite() && $blog_id) {
$this->switched = switch_to_blog($blog_id);
}
}
/**
* Function that gets called after every action
*
* @param string $command a string that corresponds to UDC command to call a certain method for this class.
* @param array $data an array of data post or get fields
* @param array $extra_info extrainfo use in the udrpc_action, e.g. user_id
*
* link to udrpc_action main function in class UpdraftCentral_Listener
*/
public function _post_action($command, $data, $extra_info) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Unused parameter is present because the caller from UpdraftCentral_Listener class uses 3 arguments.
// Here, we're restoring to the current (default) blog before we switched
if ($this->switched) restore_current_blog();
}
/**
* Constructor
*/
public function __construct() {
$this->_admin_include('theme.php', 'file.php', 'template.php', 'class-wp-upgrader.php', 'theme-install.php', 'update.php');
}
/**
* Installs and activates a theme through upload
*
* @param array $params Parameter array containing information pertaining the currently uploaded theme
* @return array Contains the result of the current process
*/
public function upload_theme($params) {
return $this->process_chunk_upload($params, 'theme');
}
/**
* Checks whether the theme is currently installed and activated.
*
* @param array $query Parameter array containing the name of the theme to check
* @return array Contains the result of the current process
*/
public function is_theme_installed($query) {
if (!isset($query['theme']))
return $this->_generic_error_response('theme_name_required');
$result = $this->_get_theme_info($query['theme']);
return $this->_response($result);
}
/**
* Applies currently requested action for theme processing
*
* @param string $action The action to apply (e.g. activate or install)
* @param array $query Parameter array containing information for the currently requested action
*
* @return array
*/
private function _apply_theme_action($action, $query) {
$result = array();
switch ($action) {
case 'activate':
$info = $this->_get_theme_info($query['theme']);
if ($info['installed']) {
switch_theme($info['slug']);
if (wp_get_theme()->get_stylesheet() === $info['slug']) {
$result = array('activated' => true, 'info' => $this->_get_theme_info($query['theme']), 'last_state' => $info);
} else {
$result = $this->_generic_error_response('theme_not_activated', array(
'theme' => $query['theme'],
'error_code' => 'theme_not_activated',
'error_message' => __('There appears to be a problem activating or switching to the intended theme.', 'updraftplus').' '.__('Please check your permissions and try again.', 'updraftplus'),
'info' => $this->_get_theme_info($query['theme'])
));
}
} else {
$result = $this->_generic_error_response('theme_not_installed', array(
'theme' => $query['theme'],
'error_code' => 'theme_not_installed',
'error_message' => __('The theme you wish to activate is either not installed or has been removed recently.', 'updraftplus'),
'info' => $info
));
}
break;
case 'network_enable':
$info = $this->_get_theme_info($query['theme']);
if ($info['installed']) {
if (current_user_can('manage_network_themes')) {
// Make sure that network_enable_theme is present and callable since
// it is only available at 4.6. If not, we'll do things the old fashion way
if (is_callable(array('WP_Theme', 'network_enable_theme'))) {
WP_Theme::network_enable_theme($info['slug']);
} else {
$allowed_themes = get_site_option('allowedthemes');
$allowed_themes[$info['slug']] = true;
update_site_option('allowedthemes', $allowed_themes);
}
}
$allowed = WP_Theme::get_allowed_on_network();
if (is_array($allowed) && !empty($allowed[$info['slug']])) {
$result = array('enabled' => true, 'info' => $this->_get_theme_info($query['theme']), 'last_state' => $info);
} else {
$result = $this->_generic_error_response('theme_not_enabled', array(
'theme' => $query['theme'],
'error_code' => 'theme_not_enabled',
'error_message' => __('There appears to be a problem enabling the intended theme on your network.', 'updraftplus').' '.__('Please kindly check your permission and try again.', 'updraftplus'),
'info' => $this->_get_theme_info($query['theme'])
));
}
} else {
$result = $this->_generic_error_response('theme_not_installed', array(
'theme' => $query['theme'],
'error_code' => 'theme_not_installed',
'error_message' => __('The theme you wish to enable on your network is either not installed or has been removed recently.', 'updraftplus'),
'info' => $info
));
}
break;
case 'network_disable':
$info = $this->_get_theme_info($query['theme']);
if ($info['installed']) {
if (current_user_can('manage_network_themes')) {
// Make sure that network_disable_theme is present and callable since
// it is only available at 4.6. If not, we'll do things the old fashion way
if (is_callable(array('WP_Theme', 'network_disable_theme'))) {
WP_Theme::network_disable_theme($info['slug']);
} else {
$allowed_themes = get_site_option('allowedthemes');
if (isset($allowed_themes[$info['slug']])) {
unset($allowed_themes[$info['slug']]);
}
update_site_option('allowedthemes', $allowed_themes);
}
}
$allowed = WP_Theme::get_allowed_on_network();
if (is_array($allowed) && empty($allowed[$info['slug']])) {
$result = array('disabled' => true, 'info' => $this->_get_theme_info($query['theme']), 'last_state' => $info);
} else {
$result = $this->_generic_error_response('theme_not_disabled', array(
'theme' => $query['theme'],
'error_code' => 'theme_not_disabled',
'error_message' => __('There appears to be a problem disabling the intended theme from your network.', 'updraftplus').' '.__('Please kindly check your permission and try again.', 'updraftplus'),
'info' => $this->_get_theme_info($query['theme'])
));
}
} else {
$result = $this->_generic_error_response('theme_not_installed', array(
'theme' => $query['theme'],
'error_code' => 'theme_not_installed',
'error_message' => __('The theme you wish to disable from your network is either not installed or has been removed recently.', 'updraftplus'),
'info' => $info
));
}
break;
case 'install':
$api = themes_api('theme_information', array(
'slug' => $query['slug'],
'fields' => array(
'description' => true,
'sections' => false,
'rating' => true,
'ratings' => true,
'downloaded' => true,
'downloadlink' => true,
'last_updated' => true,
'screenshot_url' => true,
'parent' => true,
)
));
$info = $this->_get_theme_info($query['theme']);
if (is_wp_error($api)) {
$result = $this->_generic_error_response('generic_response_error', array(
'theme' => $query['theme'],
'error_code' => 'theme_not_installed',
'error_message' => $api->get_error_message(),
'info' => $info
));
} else {
$installed = $info['installed'];
$error_code = $error_message = '';
if (!$installed) {
// WP < 3.7
if (!class_exists('Automatic_Upgrader_Skin')) include_once(dirname(dirname(__FILE__)).'/classes/class-automatic-upgrader-skin.php');
$skin = new Automatic_Upgrader_Skin();
$upgrader = new Theme_Upgrader($skin);
$download_link = $api->download_link;
$installed = $upgrader->install($download_link);
if (is_wp_error($installed)) {
$error_code = $installed->get_error_code();
$error_message = $installed->get_error_message();
} elseif (is_wp_error($skin->result)) {
$error_code = $skin->result->get_error_code();
$error_message = $skin->result->get_error_message();
$error_data = $skin->result->get_error_data($error_code);
if (!empty($error_data)) {
if (is_array($error_data)) $error_data = json_encode($error_data);
$error_message .= ' '.$error_data;
}
} elseif (is_null($installed) || !$installed) {
global $wp_filesystem;
$upgrade_messages = $skin->get_upgrade_messages();
if (!class_exists('WP_Filesystem_Base')) include_once(ABSPATH.'/wp-admin/includes/class-wp-filesystem-base.php');
// Pass through the error from WP_Filesystem if one was raised.
if ($wp_filesystem instanceof WP_Filesystem_Base && is_wp_error($wp_filesystem->errors) && $wp_filesystem->errors->get_error_code()) {
$error_code = $wp_filesystem->errors->get_error_code();
$error_message = $wp_filesystem->errors->get_error_message();
} elseif (!empty($upgrade_messages)) {
// We're only after for the last feedback that we received from the install process. Mostly,
// that is where the last error has been inserted.
$messages = $skin->get_upgrade_messages();
$error_code = 'install_failed';
$error_message = end($messages);
} else {
$error_code = 'unable_to_connect_to_filesystem';
$error_message = __('Unable to connect to the filesystem.', 'updraftplus').' '.__('Please confirm your credentials.', 'updraftplus');
}
}
}
if (!$installed || is_wp_error($installed)) {
$result = $this->_generic_error_response('theme_install_failed', array(
'theme' => $query['theme'],
'error_code' => $error_code,
'error_message' => $error_message,
'info' => $this->_get_theme_info($query['theme'])
));
} else {
$result = array('installed' => true, 'info' => $this->_get_theme_info($query['theme']), 'last_state' => $info);
}
}
break;
}
return $result;
}
/**
* Preloads the submitted credentials to the global $_POST variable
*
* @param array $query Parameter array containing information for the currently requested action
*/
private function _preload_credentials($query) {
if (!empty($query) && isset($query['filesystem_credentials'])) {
parse_str($query['filesystem_credentials'], $filesystem_credentials);
if (is_array($filesystem_credentials)) {
foreach ($filesystem_credentials as $key => $value) {
// Put them into $_POST, which is where request_filesystem_credentials() checks for them.
$_POST[$key] = $value;
}
}
}
}
/**
* Checks whether we have the required fields submitted and the user has
* the capabilities to execute the requested action
*
* @param array $query The submitted information
* @param array $fields The required fields to check
* @param array $capabilities The capabilities to check and validate
*
* @return array|string
*/
private function _validate_fields_and_capabilities($query, $fields, $capabilities) {
$error = '';
if (!empty($fields)) {
for ($i=0; $i<count($fields); $i++) {
$field = $fields[$i];
if (!isset($query[$field])) {
if ('keyword' === $field) {
$error = $this->_generic_error_response('keyword_required');
} else {
$error = $this->_generic_error_response('theme_'.$query[$field].'_required');
}
break;
}
}
}
if (empty($error) && !empty($capabilities)) {
for ($i=0; $i<count($capabilities); $i++) {
if (!current_user_can($capabilities[$i])) {
$error = $this->_generic_error_response('theme_insufficient_permission');
break;
}
}
}
return $error;
}
/**
* Processing an action for multiple items
*
* @param array $query Parameter array containing a list of themes to process
* @return array Contains the results of the bulk process
*/
public function process_action_in_bulk($query) {
$action = isset($query['action']) ? $query['action'] : '';
$items = isset($query['args']) ? $query['args']['items'] : array();
$results = array();
if (!empty($action) && !empty($items) && is_array($items)) {
foreach ($items as $value) {
if (method_exists($this, $action)) {
$results[] = $this->$action($value);
}
}
}
return $this->_response($results);
}
/**
* Activates the theme
*
* @param array $query Parameter array containing the name of the theme to activate
* @return array Contains the result of the current process
*/
public function activate_theme($query) {
$fields = array('theme');
$permissions = array('switch_themes');
$error = $this->_validate_fields_and_capabilities($query, $fields, $permissions);
if (!empty($error)) {
return $error;
}
$this->_preload_credentials($query);
$result = $this->_apply_theme_action('activate', $query);
if (empty($result['activated'])) {
return $result;
}
return $this->_response($result);
}
/**
* Enables theme for network
*
* @param array $query Parameter array containing the name of the theme to activate
* @return array Contains the result of the current process
*/
public function network_enable_theme($query) {
$fields = array('theme');
$permissions = array('switch_themes');
$error = $this->_validate_fields_and_capabilities($query, $fields, $permissions);
if (!empty($error)) {
return $error;
}
$this->_preload_credentials($query);
$result = $this->_apply_theme_action('network_enable', $query);
if (empty($result['enabled'])) {
return $result;
}
return $this->_response($result);
}
/**
* Disables theme from network
*
* @param array $query Parameter array containing the name of the theme to activate
* @return array Contains the result of the current process
*/
public function network_disable_theme($query) {
$fields = array('theme');
$permissions = array('switch_themes');
$error = $this->_validate_fields_and_capabilities($query, $fields, $permissions);
if (!empty($error)) {
return $error;
}
$this->_preload_credentials($query);
$result = $this->_apply_theme_action('network_disable', $query);
if (empty($result['disabled'])) {
return $result;
}
return $this->_response($result);
}
/**
* Download, install and activates the theme
*
* @param array $query Parameter array containing the filesystem credentials entered by the user along with the theme name and slug
* @return array Contains the result of the current process
*/
public function install_activate_theme($query) {
$fields = array('theme', 'slug');
$permissions = array('install_themes', 'switch_themes');
$error = $this->_validate_fields_and_capabilities($query, $fields, $permissions);
if (!empty($error)) {
return $error;
}
$this->_preload_credentials($query);
$result = $this->_apply_theme_action('install', $query);
if (!empty($result['installed']) && $result['installed']) {
$result = $this->_apply_theme_action('activate', $query);
if (empty($result['activated'])) {
return $result;
}
} else {
return $result;
}
return $this->_response($result);
}
/**
* Download, install the theme
*
* @param array $query Parameter array containing the filesystem credentials entered by the user along with the theme name and slug
* @return array Contains the result of the current process
*/
public function install_theme($query) {
$fields = array('theme', 'slug');
$permissions = array('install_themes');
$error = $this->_validate_fields_and_capabilities($query, $fields, $permissions);
if (!empty($error)) {
return $error;
}
$this->_preload_credentials($query);
$result = $this->_apply_theme_action('install', $query);
if (empty($result['installed'])) {
return $result;
}
return $this->_response($result);
}
/**
* Uninstall/delete the theme
*
* @param array $query Parameter array containing the filesystem credentials entered by the user along with the theme name and slug
* @return array Contains the result of the current process
*/
public function delete_theme($query) {
$fields = array('theme');
$permissions = array('delete_themes');
$error = $this->_validate_fields_and_capabilities($query, $fields, $permissions);
if (!empty($error)) {
return $error;
}
$this->_preload_credentials($query);
$info = $this->_get_theme_info($query['theme']);
if ($info['installed']) {
$deleted = delete_theme($info['slug']);
if ($deleted) {
$result = array('deleted' => true, 'info' => $this->_get_theme_info($query['theme']), 'last_state' => $info);
} else {
return $this->_generic_error_response('delete_theme_failed', array(
'theme' => $query['theme'],
'error_code' => 'delete_theme_failed',
'info' => $info
));
}
} else {
return $this->_generic_error_response('theme_not_installed', array(
'theme' => $query['theme'],
'error_code' => 'theme_not_installed',
'info' => $info
));
}
return $this->_response($result);
}
/**
* Updates/upgrade the theme
*
* @param array $query Parameter array containing the filesystem credentials entered by the user along with the theme name and slug
* @return array Contains the result of the current process
*/
public function update_theme($query) {
$fields = array('theme');
$permissions = array('update_themes');
$error = $this->_validate_fields_and_capabilities($query, $fields, $permissions);
if (!empty($error)) {
return $error;
}
$this->_preload_credentials($query);
// Make sure that we still have the theme installed before running
// the update process
$info = $this->_get_theme_info($query['theme']);
if ($info['installed']) {
// Load the updates command class if not existed
if (!class_exists('UpdraftCentral_Updates_Commands')) include_once('updates.php');
$update_command = new UpdraftCentral_Updates_Commands($this->rc);
$result = $update_command->update_theme($info['slug']);
if (!empty($result['error'])) {
$result['values'] = array('theme' => $query['theme'], 'info' => $info);
}
} else {
return $this->_generic_error_response('theme_not_installed', array(
'theme' => $query['theme'],
'error_code' => 'theme_not_installed',
'info' => $info
));
}
return $this->_response($result);
}
/**
* Gets the theme information along with its active and install status
*
* @internal
* @param array $theme The name of the theme to pull the information from
* @return array Contains the theme information
*/
private function _get_theme_info($theme) {
$info = array(
'active' => false,
'installed' => false
);
// Clear theme cache so that newly installed/downloaded themes
// gets reflected when calling "get_themes"
if (function_exists('wp_clean_themes_cache')) {
wp_clean_themes_cache();
}
// Gets all themes available.
$themes = wp_get_themes();
$current_theme_slug = basename(get_stylesheet_directory());
// Loops around each theme available.
foreach ($themes as $slug => $value) {
$name = $value->get('Name');
$theme_name = !empty($name) ? $name : $slug;
// If the theme name matches that of the specified name, it will gather details.
if ($theme_name === $theme) {
$info['installed'] = true;
$info['active'] = ($slug === $current_theme_slug) ? true : false;
$info['slug'] = $slug;
$info['data'] = $value;
$info['name'] = $theme_name;
break;
}
}
return $info;
}
/**
* Loads all available themes with additional attributes and settings needed by UpdraftCentral
*
* @param array $query Parameter array Any available parameters needed for this action
* @return array Contains the result of the current process
*/
public function load_themes($query) {
$permissions = array('install_themes', 'switch_themes');
$args = array();
if (is_multisite() && !is_super_admin(get_current_user_id())) {
$permissions = array('switch_themes');
$args = array('allowed' => true, 'blog_id' => get_current_blog_id());
}
$error = $this->_validate_fields_and_capabilities($query, array(), $permissions);
if (!empty($error)) {
return $error;
}
// Get pagination parameters early
$updates_only = isset($query['updates_only']) ? filter_var($query['updates_only'], FILTER_VALIDATE_BOOLEAN) : false;
$include_updates_pagination = isset($query['include_updates_pagination']) && $query['include_updates_pagination'];
$per_page = isset($query['per_page']) ? (int) $query['per_page'] : 10;
$page = isset($query['page']) ? (int) $query['page'] : 1;
$offset = ($page - 1) * $per_page;
// A call is considered legacy if neither updates_only nor include_updates_pagination flags are set
$is_legacy_call = !isset($query['updates_only']) && !isset($query['include_updates_pagination']) && !isset($query['per_page']);
$sort_direction = isset($query['sort_direction']) ? strtolower($query['sort_direction']) : 'asc';
// Initialize arrays
$themes_with_updates = array();
$themes_without_updates = array();
$installed_slugs = array();
$snoozed_themes_info = array();
// Get current theme info once
$current_theme_slug = basename(get_stylesheet_directory());
$website = get_bloginfo('name');
// Load updates
if (!class_exists('UpdraftCentral_Updates_Commands')) include_once('updates.php');
$updates = new UpdraftCentral_Updates_Commands($this->rc);
$theme_updates = (array) $updates->get_item_updates('themes');
// Get all themes
$themes = wp_get_themes($args);
// Sort themes by name
if (!empty($themes)) {
uasort($themes, array($this, '_sort_themes_by_name'));
}
// Get list of snoozed theme slugs if provided
$snoozed_slugs = isset($query['snoozed_themes']) ? $query['snoozed_themes'] : array();
// Prepare config object for theme data
$config = array(
'website' => $website,
'current_theme_slug' => $current_theme_slug,
'is_legacy_call' => $is_legacy_call,
'snoozed_slugs' => $snoozed_slugs
);
// Process themes efficiently
foreach ($themes as $slug => $value) {
// Skip processing if we're only looking for updates and this theme has none
if ($updates_only && empty($theme_updates[$slug])) {
continue;
}
$config['slug'] = $slug;
$theme = $this->_prepare_theme_data($value, $config);
// Track installed slugs if needed
if (!empty($query['get_installed_slugs'])) {
$installed_slugs[] = $slug;
}
// Check for updates and categorize
if (!empty($theme_updates[$slug]) && version_compare($theme->version, $theme_updates[$slug]->update['new_version'], '<')) {
$theme = $this->_add_update_info($theme, $theme_updates[$slug], $is_legacy_call);
// Add to themes_with_updates regardless of snooze status
if ($slug === $current_theme_slug) {
$themes_with_updates = array($theme->slug => $theme) + $themes_with_updates;
} else {
$themes_with_updates[$theme->slug] = $theme;
}
// Track snoozed status separately
if (in_array($slug, $snoozed_slugs)) {
$snoozed_themes_info[$slug] = $theme;
}
} else {
if ($slug === $current_theme_slug) {
$themes_without_updates = array($theme->slug => $theme) + $themes_without_updates;
} else {
$themes_without_updates[$theme->slug] = $theme;
}
}
}
// Handle response based on call type
if ($is_legacy_call) {
$result = $this->_prepare_legacy_response($themes_with_updates, $themes_without_updates, $theme_updates);
} else {
$result = $this->_prepare_paginated_response(
$themes_with_updates,
$themes_without_updates,
$updates_only,
$include_updates_pagination,
$per_page,
$page,
$offset,
$current_theme_slug,
$sort_direction
);
}
// Add installed slugs if requested
if (!empty($query['get_installed_slugs'])) {
$result['installed_slugs'] = $installed_slugs;
}
// Add snoozed themes info if any were requested
if (!empty($snoozed_themes_info)) {
$result['snoozed_themes_info'] = $snoozed_themes_info;
}
return $this->_response(array_merge($result, $this->_get_backup_credentials_settings(get_theme_root())));
}
/**
* Prepare basic theme data
*
* @param WP_Theme $value Theme object
* @param array $config Configuration array containing:
* - slug: Theme slug
* - website: Website name
* - current_theme_slug: Current active theme slug
* - is_legacy_call: Whether this is a legacy call
* - snoozed_slugs: Array of snoozed theme slugs
* @return stdClass Theme data object
*/
private function _prepare_theme_data($value, $config) {
$theme = new stdClass();
$theme->name = $value->get('Name') ? $value->get('Name') : $config['slug'];
$theme->description = $value->get('Description');
$theme->slug = $config['slug'];
$theme->version = $value->get('Version');
$theme->author = $value->get('Author');
$theme->status = ($config['slug'] === $config['current_theme_slug']) ? 'active' : 'inactive';
// Set is_snoozed property for all themes
$theme->is_snoozed = !empty($config['snoozed_slugs']) && in_array($config['slug'], $config['snoozed_slugs']);
if (!$config['is_legacy_call']) {
$screenshot = $value->get_screenshot();
$theme->screenshot = $screenshot ? $screenshot : null;
$theme->has_update = false;
}
$template = $value->get('Template');
$theme->child_theme = !empty($template);
$theme->website = $config['website'];
$theme->multisite = is_multisite();
$theme->site_url = trailingslashit(get_bloginfo('url'));
if ($theme->child_theme) {
$parent_theme = wp_get_theme($template);
$theme->parent = $parent_theme->get('Name') ? $parent_theme->get('Name') : $parent_theme->get_stylesheet();
}
if (empty($theme->short_description) && !empty($theme->description)) {
$temp = explode('.', $theme->description);
$theme->short_description = $temp[0] . (isset($temp[1]) ? '.' . $temp[1] : '') . '.';
}
return $theme;
}
/**
* Add update information to theme object
*
* @param stdClass $theme Theme object
* @param object $update_info Update information
* @param bool $is_legacy_call Whether this is a legacy call
* @return stdClass Updated theme object
*/
private function _add_update_info($theme, $update_info, $is_legacy_call) {
$theme->latest_version = $update_info->update['new_version'];
$theme->download_link = $update_info->update['package'];
if (!$is_legacy_call) {
$theme->update_url = $update_info->update['url'];
$theme->requires = $update_info->update['requires'];
$theme->requires_php = $update_info->update['requires_php'];
$theme->has_update = true;
}
return $theme;
}
/**
* Prepare legacy response format
*
* @param array $themes_with_updates Themes with updates
* @param array $themes_without_updates Themes without updates
* @param array $theme_updates Theme updates data
* @return array Legacy format response
*/
private function _prepare_legacy_response($themes_with_updates, $themes_without_updates, $theme_updates) {
return array(
'themes' => array_merge(array_values($themes_with_updates), array_values($themes_without_updates)),
'theme_updates' => $theme_updates,
'is_super_admin' => is_super_admin(),
);
}
/**
* Prepare paginated response format
*
* @param array $themes_with_updates Themes with updates
* @param array $themes_without_updates Themes without updates
* @param bool $updates_only Whether only updates are requested
* @param bool $include_updates_pagination Whether to include updates pagination
* @param int $per_page Number of items per page
* @param int $page Current page number
* @param int $offset Offset for pagination
* @param string $current_theme_slug Current active theme slug
* @param string $sort_direction Sort direction
* @return array Paginated response
*/
private function _prepare_paginated_response($themes_with_updates, $themes_without_updates, $updates_only, $include_updates_pagination, $per_page, $page, $offset, $current_theme_slug, $sort_direction) {
// If only requesting updates, filter out snoozed themes here
if ($updates_only) {
$filtered_updates = array_filter($themes_with_updates, array($this, '_filter_non_snoozed'));
$results = array_slice($filtered_updates, $offset, $per_page);
return array(
'theme_updates' => $results,
'pagination' => array(
'total' => count($filtered_updates),
'per_page' => $per_page,
'current_page' => $page,
'total_pages' => ceil(count($filtered_updates) / $per_page)
)
);
}
// For all themes view, merge and sort
$all_themes = array_merge($themes_with_updates, $themes_without_updates);
// Sort themes
global $current_theme_slug, $sort_direction;
$current_theme_slug = $current_theme_slug;
$sort_direction = $sort_direction;
uasort($all_themes, array($this, '_sort_themes_with_active'));
// Get paginated results for all themes
$results = array_slice($all_themes, $offset, $per_page);
// Filter out snoozed themes from updates list
$filtered_updates = array_filter($themes_with_updates, array($this, '_filter_non_snoozed'));
// For theme_updates, use the filtered list
$paginated_updates = $include_updates_pagination ? array_slice($filtered_updates, 0, $per_page) : $filtered_updates;
// Return with all the original data
$total_themes = count($all_themes);
$result = array(
'themes' => $results, // Contains ALL themes including snoozed ones
'theme_updates' => $paginated_updates, // Contains only non-snoozed themes with updates
'is_super_admin' => is_super_admin(),
'pagination' => array(
'total' => $total_themes,
'per_page' => $per_page,
'current_page' => $page,
'total_pages' => ceil($total_themes / $per_page)
),
'updates_count' => count($filtered_updates)
);
if ($include_updates_pagination) {
$result['updates_pagination'] = array(
'total' => count($filtered_updates),
'per_page' => $per_page,
'current_page' => 1,
'total_pages' => ceil(count($filtered_updates) / $per_page)
);
}
return $result;
}
/**
* Sort themes by name
*
* @param object $a Theme object
* @param object $b Theme object
* @return int Comparison result
*/
private function _sort_themes_by_name($a, $b) {
global $sort_direction;
$result = strcasecmp($a->name, $b->name);
return 'desc' === $sort_direction ? -$result : $result;
}
/**
* Sort all themes while keeping active theme first
*
* @param object $a Theme object
* @param object $b Theme object
* @return int Comparison result
*/
private function _sort_themes_with_active($a, $b) {
global $current_theme_slug, $sort_direction;
// Active theme always first
if ($a->slug === $current_theme_slug) return -1;
if ($b->slug === $current_theme_slug) return 1;
// Sort by name respecting direction
$result = strcasecmp($a->name, $b->name);
return 'desc' === $sort_direction ? -$result : $result;
}
/**
* Filter out snoozed themes
*
* @param object $theme Theme object
* @return bool Whether the theme is not snoozed
*/
private function _filter_non_snoozed($theme) {
return !$theme->is_snoozed;
}
/**
* Gets the backup and security credentials settings for this website
*
* @param array $query Parameter array Any available parameters needed for this action
* @return array Contains the result of the current process
*/
public function get_theme_requirements() {
return $this->_response($this->_get_backup_credentials_settings(get_theme_root()));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,632 @@
<?php
if (!defined('UPDRAFTCENTRAL_CLIENT_DIR')) die('No access.');
/**
* Handles Users Commands
*/
class UpdraftCentral_Users_Commands extends UpdraftCentral_Commands {
/**
* Compares two user object whether one is lesser than, equal to, greater than the other
*
* @internal
* @param array $a First user in the comparison
* @param array $b Second user in the comparison
* @return integer Comparison results (0 = equal, -1 = less than, 1 = greater than)
*/
private function compare_user_id($a, $b) {
if ($a->ID === $b->ID) {
return 0;
}
return ($a->ID < $b->ID) ? -1 : 1;
}
/**
* Searches users based from the keyword submitted
*
* @internal
* @param array $query Parameter array containing the filter and keyword fields
* @return array Contains the list of users found as well as the total users count
*/
private function _search_users($query) {
$this->_admin_include('user.php');
$query1 = new WP_User_Query(array(
'orderby' => 'ID',
'order' => 'ASC',
'role'=> $query["role"],
'search' => '*' . esc_attr($query["search"]) . '*',
'search_columns' => array('user_login', 'user_email')
));
$query2 = new WP_User_Query(array(
'orderby' => 'ID',
'order' => 'ASC',
'role'=> $query["role"],
'meta_query'=>array(
'relation' => 'OR',
array(
'key' => 'first_name',
'value' => $query["search"],
'compare' => 'LIKE'
),
array(
'key' => 'last_name',
'value' => $query["search"],
'compare' => 'LIKE'
),
)
));
if (empty($query1->results) && empty($query2->results)) {
return array("message" => "users_not_found");
} else {
$found_users = array_merge($query1->results, $query2->results);
$temp = array();
foreach ($found_users as $new_user) {
if (!isset($temp[$new_user->ID])) {
$temp[$new_user->ID] = $new_user;
}
};
$users = array_values($temp);
// Sort users:
usort($users, array($this, 'compare_user_id'));
$offset = ((int) $query['page_no'] * (int) $query['per_page']) - (int) $query['per_page'];
$user_list = array_slice($users, $offset, $query['per_page']);
return array(
'users' => $user_list,
'total_users' => count($users)
);
}
}
/**
* Calculates the number of pages needed to construct the pagination links
*
* @internal
* @param array $query
* @param array $total_users The total number of users found from the WP_User_Query query
* @return array Contains information needed to construct the pagination links
*/
private function _calculate_pages($query, $total_users) {
$per_page_options = array(10, 20, 30, 40, 50);
if (!empty($query)) {
$pages = array();
$page_count = ceil($total_users / $query["per_page"]);
if ($page_count > 1) {
for ($i = 0; $i < $page_count; $i++) {
if ($i + 1 == $query['page_no']) {
$paginator_item = array(
"value"=>$i+1,
"setting"=>"disabled"
);
} else {
$paginator_item = array(
"value"=>$i+1
);
}
array_push($pages, $paginator_item);
};
if ($query['page_no'] >= $page_count) {
$page_next = array(
"value"=>$page_count,
"setting"=>"disabled"
);
} else {
$page_next = array(
"value"=>$query['page_no'] + 1
);
};
if (1 === $query['page_no']) {
$page_prev = array(
"value"=>1,
"setting"=>"disabled"
);
} else {
$page_prev = array(
"value"=>$query['page_no'] - 1
);
};
return array(
"page_no" => $query['page_no'],
"per_page" => $query["per_page"],
"page_count" => $page_count,
"pages" => $pages,
"page_next" => $page_next,
"page_prev" => $page_prev,
"total_results" => $total_users,
"per_page_options" => $per_page_options
);
} else {
return array(
"page_no" => $query['page_no'],
"per_page" => $query["per_page"],
"page_count" => $page_count,
"total_results" => $total_users,
"per_page_options" => $per_page_options
);
}
} else {
return array(
"per_page_options" => $per_page_options
);
}
}
/**
* Validates whether the username exists
*
* @param array $params Contains the user name to check and validate
* @return array An array containing the result of the current process
*/
public function check_username($params) {
$this->_admin_include('user.php');
$username = $params['user_name'];
$blog_id = get_current_blog_id();
if (!empty($params['site_id'])) {
$blog_id = $params['site_id'];
}
// Here, we're switching to the actual blog that we need
// to pull users from.
$switched = function_exists('switch_to_blog') ? switch_to_blog($blog_id) : false;
if (username_exists($username) && is_user_member_of_blog(username_exists($username), $blog_id)) {
$result = array("valid" => false, "message" => 'username_exists');
return $this->_response($result);
}
if (!validate_username($username)) {
$result = array("valid" => false, "message" => 'username_invalid');
return $this->_response($result);
}
// Here, we're restoring to the current (default) blog before we
// do the switched.
if (function_exists('restore_current_blog') && $switched) {
restore_current_blog();
}
$result = array("valid" => true, "message" => 'username_valid');
return $this->_response($result);
}
/**
* Pulls blog sites available
* for the current WP instance.
* If the site is a multisite, then sites under the network
* will be pulled, otherwise, it will return an empty array.
*
* @return Array - an array of sites
*/
private function _get_blog_sites() {
if (!is_multisite()) return array();
// Initialize array container
$sites = $network_sites = array();
// Check to see if latest get_sites (available on WP version >= 4.6) function is
// available to pull any available sites from the current WP instance. If not, then
// we're going to use the fallback function wp_get_sites (for older version).
if (function_exists('get_sites') && class_exists('WP_Site_Query')) {
$network_sites = get_sites();
} else {
if (function_exists('wp_get_sites')) {
$network_sites = wp_get_sites();
}
}
// We only process if sites array is not empty, otherwise, bypass
// the next block.
if (!empty($network_sites)) {
foreach ($network_sites as $site) {
// Here we're checking if the site type is an array, because
// we're pulling the blog_id property based on the type of
// site returned.
// get_sites returns an array of object, whereas the wp_get_sites
// function returns an array of array.
$blog_id = is_array($site) ? $site['blog_id'] : $site->blog_id;
// We're saving the blog_id and blog name as an associative item
// into the sites array, that will be used as "Sites" option in
// the frontend.
$sites[$blog_id] = get_blog_details($blog_id)->blogname;
}
}
return $sites;
}
/**
* Validates whether the email exists
*
* @param array $params Contains the email to check and validate
* @return array An array containing the result of the current process
*/
public function check_email($params) {
$this->_admin_include('user.php');
$email = $params['email'];
$blog_id = get_current_blog_id();
if (isset($params['site_id']) && 0 !== $params['site_id']) {
$blog_id = $params['site_id'];
}
// Here, we're switching to the actual blog that we need
// to pull users from.
$switched = false;
if (function_exists('switch_to_blog')) {
$switched = switch_to_blog($blog_id);
}
if (is_email($email) === false) {
$result = array("valid" => false, "message" => 'email_invalid');
return $this->_response($result);
}
if (email_exists($email) && is_user_member_of_blog(email_exists($email), $blog_id)) {
$result = array("valid" => false, "message" => 'email_exists');
return $this->_response($result);
}
// Here, we're restoring to the current (default) blog before we
// do the switched.
if (function_exists('restore_current_blog') && $switched) {
restore_current_blog();
}
$result = array("valid" => true, "message" => 'email_valid');
return $this->_response($result);
}
/**
* The get_users function pull all the users from the database
* based on the current search parameters/filters. Please see _search_users
* for the breakdown of these parameters.
*
* @param array $query Parameter array containing the filter and keyword fields
* @return array An array containing the result of the current process
*/
public function get_users($query) {
$this->_admin_include('user.php');
// Here, we're getting the current blog id. If blog id
// is passed along with the parameters then we override
// that current (default) value with the parameter blog id value.
$blog_id = get_current_blog_id();
if (isset($query['site_id']) && 0 !== $query['site_id']) $blog_id = $query['site_id'];
// Here, we're switching to the actual blog that we need
// to pull users from.
$switched = false;
if (function_exists('switch_to_blog')) {
$switched = switch_to_blog($blog_id);
}
// Set default:
if (empty($query["per_page"])) {
$query["per_page"] = 10;
}
if (empty($query['page_no'])) {
$query['page_no'] = 1;
}
if (empty($query["role"])) {
$query["role"] = "";
}
$users = array();
$total_users = 0;
if (!empty($query["search"])) {
$search_results = $this->_search_users($query);
if (isset($search_results['users'])) {
$users = $search_results['users'];
$total_users = $search_results['total_users'];
}
} else {
$user_query = new WP_User_Query(array(
'orderby' => 'ID',
'order' => 'ASC',
'number' => $query["per_page"],
'paged'=> $query['page_no'],
'role'=> $query["role"]
));
if (empty($user_query->results)) {
$result = array("message" => 'users_not_found');
return $this->_response($result);
}
$users = $user_query->results;
$total_users = $user_query->get_total();
}
foreach ($users as &$user) {
$user_object = get_userdata($user->ID);
if (method_exists($user_object, 'to_array')) {
$user = $user_object->to_array();
$user["roles"] = $user_object->roles;
$user["first_name"] = $user_object->first_name;
$user["last_name"] = $user_object->last_name;
$user["description"] = $user_object->description;
} else {
$user = $user_object;
}
}
$result = array(
"users"=>$users,
"paging" => $this->_calculate_pages($query, $total_users)
);
// Here, we're restoring to the current (default) blog before we
// do the switched.
if (function_exists('restore_current_blog') && $switched) {
restore_current_blog();
}
return $this->_response($result);
}
/**
* Creates new user for the current blog
*
* @param array $user User information to add
* @return array An array containing the result of the current process
*/
public function add_user($user) {
$this->_admin_include('user.php');
// Here, we're getting the current blog id. If blog id
// is passed along with the parameters then we override
// that current (default) value with the parameter blog id value.
$blog_id = get_current_blog_id();
if (isset($user['site_id']) && 0 !== $user['site_id']) $blog_id = $user['site_id'];
// Here, we're switching to the actual blog that we need
// to pull users from.
$switched = false;
if (function_exists('switch_to_blog')) {
$switched = switch_to_blog($blog_id);
}
if (!current_user_can('create_users') && !is_super_admin()) {
$result = array('error' => true, 'message' => 'user_create_no_permission', 'data' => array('multisite' => is_multisite()));
return $this->_response($result);
}
if (is_email($user["user_email"]) === false) {
$result = array("error" => true, "message" => "email_invalid");
return $this->_response($result);
}
if (email_exists($user["user_email"]) && is_user_member_of_blog(email_exists($user["user_email"]), $blog_id)) {
$result = array("error" => true, "message" => "email_exists");
return $this->_response($result);
}
if (username_exists($user["user_login"]) && is_user_member_of_blog(username_exists($user["user_login"]), $blog_id)) {
$result = array("error" => true, "message" => "username_exists");
return $this->_response($result);
}
if (!validate_username($user["user_login"])) {
$result = array("error" => true, "message" => 'username_invalid');
return $this->_response($result);
}
if (isset($user['site_id']) && !current_user_can('manage_network_users')) {
$result = array("error" => true, "message" => 'user_create_no_permission');
return $this->_response($result);
}
if (email_exists($user["user_email"]) && !is_user_member_of_blog(email_exists($user["user_email"]), $blog_id)) {
$user_id = email_exists($user["user_email"]);
} else {
$user_id = wp_insert_user($user);
}
$role = $user['role'];
if (is_multisite()) {
add_existing_user_to_blog(array('user_id' => $user_id, 'role' => $role));
}
// Here, we're restoring to the current (default) blog before we
// do the switched.
if (function_exists('restore_current_blog') && $switched) {
restore_current_blog();
}
if ($user_id > 0) {
$result = array("error" => false, "message" => "user_created_with_user_name", "values" => array($user['user_login']));
return $this->_response($result);
} else {
$result = array("error" => true, "message" => "user_create_failed", "values" => array($user));
}
return $this->_response($result);
}
/**
* [delete_user - UCP: users.delete_user]
*
* This function is used to check to make sure the user_id is valid and that it has has user delete permissions.
* If there are no issues, the user is deleted.
*
* current_user_can: This check the user permissions from UCP
* get_userdata: This get the user data on the data from user_id in the $user_id array
* wp_delete_user: Deleting users on the User ID (user_id) and, IF Specified, the Assigner ID (assign_user_id).
*
* @param [type] $params [description] THis is an Array of params sent over from UpdraftCentral
* @return [type] Array [description] This will send back an error array along with message if there are any issues with the user_id
*/
public function delete_user($params) {
$this->_admin_include('user.php');
$user_id = $params['user_id'];
$assign_user_id = $params["assign_user_id"];
// Here, we're getting the current blog id. If blog id
// is passed along with the parameters then we override
// that current (default) value with the parameter blog id value.
$blog_id = get_current_blog_id();
if (isset($params['site_id']) && 0 !== $params['site_id']) $blog_id = $params['site_id'];
$switched = false;
if (function_exists('switch_to_blog')) {
$switched = switch_to_blog($blog_id);
}
if (!current_user_can('delete_users') && !is_super_admin()) {
$result = array('error' => true, 'message' => 'user_delete_no_permission', 'data' => array('multisite' => is_multisite()));
return $this->_response($result);
}
if (get_userdata($user_id) === false) {
$result = array("error" => true, "message" => "user_not_found");
return $this->_response($result);
}
if (wp_delete_user($user_id, $assign_user_id)) {
$result = array("error" => false, "message" => "user_deleted");
} else {
$result = array("error" => true, "message" => "user_delete_failed");
}
// Here, we're restoring to the current (default) blog before we
// do the switched.
if (function_exists('restore_current_blog') && $switched) {
restore_current_blog();
}
return $this->_response($result);
}
/**
* Edits existing user information
*
* @param array $user User information to save
* @return array An array containing the result of the current process
*/
public function edit_user($user) {
$this->_admin_include('user.php');
// Here, we're getting the current blog id. If blog id
// is passed along with the parameters then we override
// that current (default) value with the parameter blog id value.
$blog_id = get_current_blog_id();
if (isset($user['site_id']) && 0 !== $user['site_id']) $blog_id = $user['site_id'];
// Here, we're switching to the actual blog that we need
// to apply our changes.
$switched = false;
if (function_exists('switch_to_blog')) {
$switched = switch_to_blog($blog_id);
}
if (!current_user_can('edit_users') && !is_super_admin() && get_current_user_id() !== $user["ID"]) {
$result = array('error' => true, 'message' => 'user_edit_no_permission', 'data' => array('multisite' => is_multisite()));
return $this->_response($result);
}
if (false === get_userdata($user["ID"])) {
$result = array("error" => true, "message" => "user_not_found");
return $this->_response($result);
}
if (get_current_user_id() == $user["ID"]) {
unset($user["role"]);
}
/* Validate Username*/
if (!validate_username($user["user_login"])) {
$result = array("error" => true, "message" => 'username_invalid');
return $this->_response($result);
}
/* Validate Email if not the same*/
$remote_user = get_userdata($user["ID"]);
$old_email = $remote_user->user_email;
if ($user['user_email'] !== $old_email) {
if (is_email($user['user_email']) === false) {
$result = array("error" => true, "message" => 'email_invalid');
return $this->_response($result);
}
if (email_exists($user['user_email'])) {
$result = array("error" => true, "message" => 'email_exists');
return $this->_response($result);
}
}
$user_id = wp_update_user($user);
if (is_wp_error($user_id)) {
$result = array("error" => true, "message" => "user_edit_failed_with_error", "values" => array($user_id));
} else {
$result = array("error" => false, "message" => "user_edited_with_user_name", "values" => array($user["user_login"]));
}
// Here, we're restoring to the current (default) blog before we
// do the switched.
if (function_exists('restore_current_blog') && $switched) {
restore_current_blog();
}
return $this->_response($result);
}
/**
* Retrieves available roles to be used as filter options
*
* @return array An array containing all available roles
*/
public function get_roles() {
$this->_admin_include('user.php');
$roles = array_reverse(get_editable_roles());
return $this->_response($roles);
}
/**
* Retrieves information to be use as filters
*
* @return array An array containing the filter fields and their data
*/
public function get_user_filters() {
$this->_admin_include('user.php');
// Pull sites options if available.
$sites = $this->_get_blog_sites();
$result = array(
"sites" => $sites,
"roles" => array_reverse(get_editable_roles()),
"paging" => $this->_calculate_pages(null, 0),
);
return $this->_response($result);
}
}