<?php
// phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_fclose, WordPress.WP.AlternativeFunctions.file_system_operations_fopen, WordPress.WP.AlternativeFunctions.file_system_operations_fwrite, WordPress.WP.AlternativeFunctions.file_system_operations_fgets, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPress.WP.AlternativeFunctions.file_system_operations_mkdir, WordPress.WP.AlternativeFunctions.file_system_operations_fread, WordPress.WP.AlternativeFunctions.file_system_operations_chmod, WordPress.WP.AlternativeFunctions.file_system_operations_fputs, WordPress.WP.AlternativeFunctions.file_system_operations_is_writable, WordPress.WP.AlternativeFunctions.file_system_operations_chown, WordPress.WP.AlternativeFunctions.file_system_operations_chgrp, WordPress.WP.AlternativeFunctions.file_system_operations_touch -- Native PHP fileystem function is used for direct control and performance because it can bypass additional layers of abstraction so that no overhead from the WordPress filesystem API's internal handling
// phpcs:disable WordPress.WP.AlternativeFunctions.rename_rename -- rename() usage is intentional and safe within this context.
if (!defined('ABSPATH')) die('No direct access allowed');

/**
 * - 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 : 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));
		}
	}
}
