<?php
if (!defined('UPDRAFTCENTRAL_CLIENT_DIR')) die('No access.');
/**
* - A container for all the RPC commands implemented. 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.
*/
abstract class UpdraftCentral_Commands {
protected $rc;
protected $ud;
protected $installed_data;
/**
* Class constructor
*
* @param string $rc
*/
public function __construct($rc) {
$this->rc = $rc;
global $updraftplus;
$this->ud = $updraftplus;
$this->installed_data = array();
}
/**
* Include a file or files from wp-admin/includes
*/
final protected function _admin_include() {
$files = func_get_args();
foreach ($files as $file) {
include_once(ABSPATH.'/wp-admin/includes/'.$file);
}
}
/**
* Include a file or files from wp-includes
*/
final protected function _frontend_include() {
$files = func_get_args();
foreach ($files as $file) {
include_once(ABSPATH.WPINC.'/'.$file);
}
}
/**
* Return a response in the expected format
*
* @param Mixed $data
* @param String $code
*
* @return Array
*/
final protected function _response($data = null, $code = 'rpcok') {
return array(
'response' => $code,
'data' => $data
);
}
/**
* Return an error in the expected format
*
* @param String $code
* @param Mixed $data
*
* @return Array
*/
final protected function _generic_error_response($code = 'central_unspecified', $data = null) {
return $this->_response(
array(
'code' => $code,
'data' => $data
),
'rpcerror'
);
}
/**
* Checks whether a backup and a security credentials is required for the given request
*
* @param array $dir The directory location to check
* @return array
*/
final protected function _get_backup_credentials_settings($dir) {
// Do we need to ask the user for filesystem credentials? when installing and/or deleting items in the given directory
$filesystem_method = get_filesystem_method(array(), $dir);
ob_start();
$filesystem_credentials_are_stored = request_filesystem_credentials(site_url(), $filesystem_method);
ob_end_clean();
$request_filesystem_credentials = ('direct' != $filesystem_method && !$filesystem_credentials_are_stored);
// Do we need to execute a backup process before installing/managing items
$automatic_backups = (class_exists('UpdraftPlus_Options') && class_exists('UpdraftPlus_Addon_Autobackup') && UpdraftPlus_Options::get_updraft_option('updraft_autobackup_default', true)) ? true : false;
return array(
'request_filesystem_credentials' => $request_filesystem_credentials,
'automatic_backups' => $automatic_backups
);
}
/**
* Retrieves the information of the currently installed item (e.g. plugin or theme) through filter
*
* @param bool $response Indicates whether the installation was a success or failure
* @param array $args Extra argument for the hook
* @param array $data Contains paths used and other relevant information regarding the file
* @return array
*/
final public function get_install_data($response, $args, $data) {
if ($response) {
switch ($args['type']) {
case 'plugin':
$plugin_data = get_plugins('/'.$data['destination_name']);
if (!empty($plugin_data)) {
$info = reset($plugin_data);
$key = key($plugin_data);
$info['slug'] = $data['destination_name'].'/'.$key;
$this->installed_data = $info;
}
break;
case 'theme':
$theme = wp_get_theme($data['destination_name']);
if ($theme->exists()) {
// Minimalistic info here, if you need to add additional information
// you can add them here. For now, the "Name" and "slug" fields will suffice
// in the succeeding process.
$this->installed_data = array(
'Name' => $theme->get('Name'),
'slug' => $data['destination_name'],
'template' => $theme->get_template()
);
}
break;
default:
break;
}
}
return $response;
}
/**
* Installs and activates either a plugin or theme through zip file upload
*
* @param array $params Parameter array containing information pertaining the currently uploaded item
* @param string $type Indicates whether this current process is intended for a 'plugin' or a 'theme' item
* @return array
*/
final protected function process_chunk_upload($params, $type) {
global $updraftcentral_host_plugin, $updraftcentral_main;
if (!in_array($type, array('plugin', 'theme'))) {
return $this->_generic_error_response('upload_type_not_supported');
}
$permission_error = false;
if ('plugin' === $type) {
if (!current_user_can('install_plugins') || !current_user_can('activate_plugins')) $permission_error = true;
} else {
if (!current_user_can('install_themes') || !current_user_can('switch_themes')) $permission_error = true;
}
if ($permission_error) {
return $this->_generic_error_response($type.'_insufficient_permission');
}
// Pull any available and writable directory where we can store
// our data/file temporarily before running the installation process.
$upload_dir = untrailingslashit(get_temp_dir());
if (!is_writable($upload_dir)) {
$upload_dir = WP_CONTENT_DIR.'/upgrade';
if (!is_dir($upload_dir)) {
$wp_dir = wp_upload_dir();
if (!empty($wp_dir['basedir'])) $upload_dir = $wp_dir['basedir'];
}
}
// If we haven't found any writable directory to temporarily store our file then
// we bail and send an error back to the caller.
if (!is_dir($upload_dir) || !is_writable($upload_dir)) {
return $this->_generic_error_response('upload_dir_not_available');
}
// Preloads the submitted credentials to the global $_POST variable
if (!empty($params) && isset($params['filesystem_credentials'])) {
parse_str($params['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;
}
}
}
// Save uploaded file
$filename = basename($params['filename']).md5(get_home_url());
$is_chunked = false;
if (isset($params['chunks']) && 1 < (int) $params['chunks']) {
$filename .= '.part';
$is_chunked = true;
}
if (!$is_chunked || ($is_chunked && isset($params['chunk']) && 0 === (int) $params['chunk'])) {
// if it's not a chunk upload or if it's a chunk upload operation and the current chunk variable is zero, then it means a new upload operation has just begun therefore we should remove previous left-over file (if any and due to error during the previous upload of the same file), because it can lead to a corrupt/invalid zip file (we use file_put_contents a few lines below with FILE_APPEND attribute)
if (file_exists($upload_dir.'/'.$filename) && !unlink($upload_dir.'/'.$filename)) return $this->_generic_error_response('unable_to_delete_existing_file');
}
if (empty($params['data'])) {
return $this->_generic_error_response('data_empty_or_invalid');
}
$result = file_put_contents($upload_dir.'/'.$filename, base64_decode($params['data']), FILE_APPEND | LOCK_EX);
if (false === $result) {
return $this->_generic_error_response('unable_to_write_content');
}
// Set $install_now to true for single upload and for the last chunk of a multi-chunks upload process
$install_now = true;
if ($is_chunked) {
if ($params['chunk'] == (int) $params['chunks'] - 1) {
// If this is the last chunk of the request, then we're going to restore the
// original filename of the file (without the '.part') since our upload is now complete.
$orig_filename = basename($filename, '.part');
$success = rename($upload_dir.'/'.$filename, $upload_dir.'/'.$orig_filename);
// If renaming the file was successful then restore the original name and override the $filename variable.
// Overriding the $filename variable makes it easy for us to use the same variable for both
// non-chunked and chunked zip file for the installation process.
if ($success) {
$filename = $orig_filename;
} else {
return $this->_generic_error_response('unable_to_rename_file');
}
} else {
// Bypass installation for now since we're waiting for the last chunk to arrive
// to complete the uploading of the zip file.
$install_now = false;
}
}
// Everything is already good (upload completed), thus, we proceed with the installation
if ($install_now) {
// We have successfully uploaded the zip file in this location with its original filename intact.
$zip_filepath = $upload_dir.'/'.$filename;
// Making sure that the file does actually exists, since we've just run through
// a renaming process above.
if (file_exists($zip_filepath)) {
add_filter('upgrader_post_install', array($this, 'get_install_data'), 10, 3);
// WP < 3.7
if (!class_exists('Automatic_Upgrader_Skin')) include_once(UPDRAFTCENTRAL_CLIENT_DIR.'/classes/class-automatic-upgrader-skin.php');
require_once(UPDRAFTCENTRAL_CLIENT_DIR.'/classes/class-updraftcentral-wp-upgrader.php');
$skin = new Automatic_Upgrader_Skin();
$upgrader = ('plugin' === $type) ? new UpdraftCentral_Plugin_Upgrader($skin) : new UpdraftCentral_Theme_Upgrader($skin);
$install_result = $upgrader->install($zip_filepath);
remove_filter('upgrader_post_install', array($this, 'get_install_data'), 10, 3);
// Remove zip file on success and on error (cleanup)
if ($install_result || is_null($install_result) || is_wp_error($install_result)) {
@unlink($zip_filepath);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise if the file doesn't exist.
}
if (false === $install_result || is_wp_error($install_result)) {
$message = $updraftcentral_host_plugin->retrieve_show_message('unable_to_connect');
if (is_wp_error($install_result)) $message = $install_result->get_error_message();
return $this->_generic_error_response($type.'_install_failed', array('message' => $message));
} else {
// Pull installed data
$data = $this->installed_data;
// For WP 3.4 the intended filter hook isn't working or not available
// so we're going to pull the data manually.
if ($install_result && empty($data)) {
$result = $this->get_install_data($install_result, array('type' => $type), $skin->result);
if ($result) {
// Getting the installed data one more time after manually calling
// the "get_install_data" function.
$data = $this->installed_data;
}
}
if (!empty($data)) {
// Activate item if set
$is_active = ('plugin' === $type) ? is_plugin_active($data['slug']) : ((wp_get_theme()->get('Name') === $data['Name']) ? true : false);
if ((bool) $params['activate'] && !$is_active) {
if ('plugin' === $type) {
if (is_multisite()) {
$activate = activate_plugin($data['slug'], '', true);
} else {
$activate = activate_plugin($data['slug']);
}
} else {
// In order to make it compatible with older versions of switch_theme which takes two
// arguments we're going to pass two arguments instead of one. Latest versions have backward
// compatibility so it's safe to do it this way.
switch_theme($data['template'], $data['slug']);
$activate = (wp_get_theme()->get_stylesheet() === $data['slug']) ? true : false;
}
if (false === $activate || is_wp_error($activate)) {
$wp_version = $updraftcentral_main->get_wordpress_version();
$message = is_wp_error($activate) ? array('message' => $activate->get_error_message()) : array('message' => sprintf($updraftcentral_host_plugin->retrieve_show_message('unable_to_activate'), $type, $type, $wp_version));
return $this->_generic_error_response('unable_to_activate_'.$type, $message);
}
}
return $this->_response(
array(
'installed' => true,
'installed_data' => $data,
)
);
}
if (is_wp_error($skin->result)) {
$code = $skin->result->get_error_code();
$message = $skin->result->get_error_message();
$error_data = $skin->result->get_error_data($code);
if (!empty($error_data)) {
if (is_array($error_data)) $error_data = json_encode($error_data);
$message .= ' '.$error_data;
}
return $this->_generic_error_response($code, $message);
} else {
return $this->_response(
array(
'installed' => false,
'message' => sprintf($updraftcentral_host_plugin->retrieve_show_message('unable_to_install'), $type, $type, $type, $type, 'wp-content/'.$type.'s'),
)
);
}
}
}
} else {
// Returning response to a chunk requests while still processing and
// completing the file upload process. If we don't return a positive response
// for every chunk requests then the caller will assumed an error has occurred
// and will eventually stop the upload process.
return $this->_response(array('in_progress' => true));
}
}
}