File manager - Edit - /home/decorea/www/wp-includes/certificates/XML/methods.tar
Back
updraftvault.php 0000644 00000152435 15231511727 0010014 0 ustar 00 <?php if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed.'); updraft_try_include_file('methods/s3.php', 'require_once'); class UpdraftPlus_BackupModule_updraftvault extends UpdraftPlus_BackupModule_s3 { private $vault_mothership = 'https://vault.updraftplus.com/plugin-info/'; private $vault_config; /** * Decides whether vault config should be printed or not * * @var Boolean */ private $vault_in_config_print; protected $quota_transient_used = false; protected $provider_can_use_aws_sdk = true; protected $provider_has_regions = true; /** * Register backup-related hooks (filters and actions) that get called in the parent method for uploading backup archives * * @param Array $backup_array - a list of file names (basenames) (within UD's directory) to be uploaded * * @return Mixed - return (boolean)false to indicate failure, or anything else to have it passed back at the delete stage (most useful for a storage object). */ public function backup($backup_array) { add_filter('updraft_updraftvault_storageclass', array($this, 'maybe_switch_to_ia_storage_class')); return parent::backup($backup_array); } /** * Determine whether to switch to IA S3 storage class rather than use the existing one (WordPress filter updraft_updraftvault_storageclass) * * @param String $class Suggested storage class * * @return String Filtered value */ public function maybe_switch_to_ia_storage_class($class) { $files_schedule = UpdraftPlus_Options::get_updraft_option('updraft_interval'); $db_schedule = UpdraftPlus_Options::get_updraft_option('updraft_interval_database'); $retain_files = max(1, (int) UpdraftPlus_Options::get_updraft_option('updraft_retain')); $retain_db = max(1, (int) UpdraftPlus_Options::get_updraft_option('updraft_retain_db')); $schedules = wp_get_schedules(); // $schedules will not contain the variable indexed by key named 'manual', so if it's a manual backup then the code below won't do anything switch ($this->current_upload_entity) { // 1296000 = 15 days in seconds case 'databases': if ('' !== $db_schedule && isset($schedules[$db_schedule]) && $schedules[$db_schedule]['interval'] * $retain_db > 1296000) $class = 'STANDARD_IA'; break; case 'files': if ('' !== $files_schedule && isset($schedules[$files_schedule]) && $schedules[$files_schedule]['interval'] * $retain_files > 1296000) $class = 'STANDARD_IA'; break; } return $class; } /** * This function makes testing easier, rather than having to change the URLs in multiple places * * @param Boolean|string $which_page specifies which page to get the URL for * @return String */ private function get_url($which_page = false) { $base = defined('UPDRAFTPLUS_VAULT_SHOP_BASE') ? UPDRAFTPLUS_VAULT_SHOP_BASE : 'https://updraftplus.com/shop/'; switch ($which_page) { case 'get_more_quota': return apply_filters('updraftplus_com_link', $base.'product-category/updraftplus-vault/'); break; case 'more_vault_info_faqs': return apply_filters('updraftplus_com_link', 'https://updraftplus.com/support/updraftplus-vault-faqs/'); break; case 'more_vault_info_landing': return apply_filters('updraftplus_com_link', 'https://updraftplus.com/landing/vault'); break; case 'vault_forgotten_credentials_links': return apply_filters('updraftplus_com_link', 'https://updraftplus.com/my-account/lost-password/'); break; default: return apply_filters('updraftplus_com_link', $base); break; } } /** * This method overrides the parent method and lists the supported features of this remote storage option. * * @return Array - an array of supported features (any features not mentioned are asuumed to not be supported) */ public function get_supported_features() { // This options format is handled via only accessing options via $this->get_options() return array('multi_options', 'config_templates', 'conditional_logic'); } /** * Retrieve default options for this remote storage module. * * @return Array - an array of options */ public function get_default_options() { return array( 'token' => '', 'email' => '', 'quota' => -1 ); } /** * Retrieve specific options for this remote storage module * * @param Array $config an array of config options * @return Array - an array of options */ protected function vault_set_config($config) { $config['whoweare'] = 'UpdraftVault'; $config['whoweare_long'] = __('UpdraftVault', 'updraftplus'); $config['key'] = 'updraftvault'; $this->vault_config = $config; } /** * Gets the UpdraftVault configuration and credentials * * @param Boolean $force_refresh - if set, and if relevant, don't use cached credentials, but get them afresh * * @return Array An array containing the Amazon S3 credentials (accesskey, secretkey, etc.) * along with some configuration values. */ public function get_config($force_refresh = false) { global $updraftplus; if (!$force_refresh) { // Have we already done this? if (!empty($this->vault_config)) return $this->vault_config; // Stored in the job? if ($job_config = $this->jobdata_get('config', null, 'updraftvault_config')) { if (!empty($job_config) && is_array($job_config)) { $this->vault_config = $job_config; return $job_config; } } } // Pass back empty settings, if nothing better can be found - this ensures that the error eventually is raised in the right place $config = array('accesskey' => '', 'secretkey' => '', 'path' => ''); $config['whoweare'] = 'Updraft Vault'; $config['whoweare_long'] = __('Updraft Vault', 'updraftplus'); $config['key'] = 'updraftvault'; // Get the stored options $opts = $this->get_options(); if (!is_array($opts) || empty($opts['token']) || empty($opts['email'])) { // Not connected. Skip DB so that it doesn't show in the UI, which confuses people (e.g. when rescanning remote storage) $this->log('this site has not been connected - check your settings', 'notice', false, true); $config['error'] = array('message' => 'site_not_connected', 'values' => array()); $this->vault_config = $config; $this->jobdata_set('config', $config); return $config; } $site_id = $updraftplus->siteid(); $this->log("requesting access details (sid=$site_id, email=".$opts['email'].")"); // Request the credentials using our token $post_body = array( 'e' => (string) $opts['email'], 'sid' => $site_id, 'token' => (string) $opts['token'], 'su' => base64_encode(home_url()) ); if (!empty($this->vault_in_config_print)) { // In this case, all that the get_config() is being done for is to get the quota info. Send back the cached quota info instead (rather than have an HTTP trip every time the settings page is loaded). The config will get updated whenever there's a backup, or the user presses the link to update. $getconfig = get_transient('udvault_last_config'); } // Use SSL to prevent snooping if (empty($getconfig) || !is_array($getconfig) || empty($getconfig['accesskey'])) { $config_array = apply_filters('updraftplus_vault_config_add_headers', array('timeout' => 25, 'body' => $post_body)); $getconfig = wp_remote_post($this->vault_mothership.'/?udm_action=vault_getconfig', $config_array); } $details_retrieved = false; $cache_in_job = false; if (!is_wp_error($getconfig) && false != $getconfig && isset($getconfig['body'])) { $response_code = wp_remote_retrieve_response_code($getconfig); if ($response_code >= 200 && $response_code < 300) { $response = json_decode(wp_remote_retrieve_body($getconfig), true); if (is_array($response) && isset($response['user_messages']) && is_array($response['user_messages'])) { foreach ($response['user_messages'] as $message) { if (!is_array($message)) continue; $msg_txt = $this->vault_translate_remote_message($message['message'], $message['code']); $this->log($msg_txt, $message['level'], $message['code']); } } if (is_array($response) && isset($response['accesskey']) && isset($response['secretkey']) && isset($response['path'])) { $details_retrieved = true; $cache_in_job = true; $opts['last_config']['accesskey'] = $response['accesskey']; $opts['last_config']['secretkey'] = $response['secretkey']; $opts['last_config']['path'] = $response['path']; unset($opts['last_config']['quota_root']); if (!empty($response['quota_root'])) { $opts['last_config']['quota_root'] = $response['quota_root']; $config['quota_root'] = $response['quota_root']; $opts['quota_root'] = $response['quota_root']; } $opts['last_config']['time'] = time(); // This is just a cache of the most recent setting if (isset($response['quota'])) { $opts['quota'] = $response['quota']; $config['quota'] = $response['quota']; } $this->set_options($opts, true); $config['accesskey'] = $response['accesskey']; $config['secretkey'] = $response['secretkey']; $config['path'] = $response['path']; $config['sessiontoken'] = (isset($response['sessiontoken']) ? $response['sessiontoken'] : ''); $config['provider'] = !empty($response['provider']) ? $response['provider'] : 'amazonaws'; } elseif (is_array($response) && isset($response['result']) && ('token_unknown' == $response['result'] || 'site_duplicated' == $response['result'])) { $this->log("This site appears to not be connected to UpdraftVault (".$response['result'].")"); $config['error'] = array('message' => 'site_not_connected', 'values' => array($response['result'])); $config['accesskey'] = ''; $config['secretkey'] = ''; $config['path'] = ''; $config['sessiontoken'] = ''; unset($config['quota']); if (!empty($response['message'])) $config['error_message'] = $response['message']; $details_retrieved = true; $cache_in_job = true; } elseif (is_array($response) && isset($response['result']) && 'error' == $response['result'] && 'gettempcreds_exception2' == $response['code']) { $this->log("An error occurred while fetching your Vault credentials. Please try again after a few minutes (".$response['code'].")"); $config['error'] = array('message' => 'fetch_credentials_error', 'values' => array($response['code'])); $config['accesskey'] = ''; $config['secretkey'] = ''; $config['path'] = ''; $config['sessiontoken'] = ''; $config['email'] = $opts['email']; // Pass along the email address used, as we need it to display our error message correctly unset($config['quota']); // We want to hide the AWS error message in this case $config['error_message'] = __('An error occurred while fetching your Vault credentials.', 'updraftplus').' '.__('Please try again after a few minutes.', 'updraftplus'); $details_retrieved = true; $cache_in_job = true; } else { if (is_array($response) && !empty($response['result'])) { $cache_in_job = true; $msg = "response code: ".$response['result']; if (!empty($response['code'])) $msg .= " (".$response['code'].")"; if (!empty($response['message'])) $msg .= " (".$response['message'].")"; if (!empty($response['data'])) $msg .= " (".json_encode($response['data']).")"; $this->log($msg); $config['error'] = array('message' => 'general_error_response', 'values' => array($msg)); } else { $this->log("Received response, but it was not in the expected format: ".substr(wp_remote_retrieve_body($getconfig), 0, 100).' ...'); $config['error'] = array('message' => 'unexpected_format', 'values' => array(substr(wp_remote_retrieve_body($getconfig), 0, 100).' ...')); } } } else { $this->log("Unexpected HTTP response code (please try again later): ".$response_code); $config['error'] = array('message' => 'unexpected_http_response', 'values' => array($response_code)); } } elseif (is_wp_error($getconfig)) { $updraftplus->log_wp_error($getconfig); $config['error'] = array('message' => 'general_error_response', 'values' => array($getconfig)); } else { if (!isset($getconfig['accesskey'])) { $this->log("wp_remote_post returned a result that was not understood (".gettype($getconfig).")"); $config['error'] = array('message' => 'result_not_understood', 'values' => array(gettype($getconfig))); } } if (!$details_retrieved) { // Don't log anything yet, as this will replace the most recently logged message in the main panel if (!empty($opts['last_config']) && is_array($opts['last_config'])) { $last_config = $opts['last_config']; if (!empty($last_config['time']) && is_numeric($last_config['time']) && $last_config['time'] > time() - 86400*15) { if ($updraftplus->backup_time) $this->log("failed to retrieve access details from updraftplus.com: will attempt to use most recently stored configuration"); if (!empty($last_config['accesskey'])) $config['accesskey'] = $last_config['accesskey']; if (!empty($last_config['secretkey'])) $config['secretkey'] = $last_config['secretkey']; if (isset($last_config['path'])) $config['path'] = $last_config['path']; if (isset($opts['quota'])) $config['quota'] = $opts['quota']; $cache_in_job = true; } else { if ($updraftplus->backup_time) $this->log("failed to retrieve access details from updraftplus.com: no recently stored configuration was found to use instead"); } } } $config['server_side_encryption'] = 'AES256'; $this->vault_config = $config; if ($cache_in_job) $this->jobdata_set('config', $config); // N.B. This isn't multi-server compatible set_transient('udvault_last_config', $config, 86400*7); return $config; } /** * Whether to always use server-side encryption - which, with Vault, we do (and our marketing says so). * * @return Boolean */ protected function use_sse() { return true; } public function vault_translate_remote_message($message, $code) { switch ($code) { case 'premium_overdue': return __('Your UpdraftPlus Premium purchase is over a year ago.', 'updraftplus').' '.__('You should renew immediately to avoid losing the 12 months of free storage allowance that you get for being a current UpdraftPlus Premium customer.', 'updraftplus'); break; case 'vault_subscription_overdue': return __('You have an UpdraftPlus Vault subscription with overdue payment.', 'updraftplus').' '.__('You are within the few days of grace period before it will be suspended, and you will lose your quota and access to data stored within it.', 'updraftplus').' '.__('Please renew as soon as possible!', 'updraftplus'); break; case 'vault_subscription_suspended': return __("You have an UpdraftPlus Vault subscription that has not been renewed, and the grace period has expired.", 'updraftplus').' '.__("In a few days' time, your stored data will be permanently removed.", 'updraftplus').' '.__("If you do not wish this to happen, then you should renew as soon as possible.", 'updraftplus'); // The following shouldn't be a possible response (the server can deal with duplicated sites with the same IDs) - but there's no harm leaving it in for now (Dec 2015) // This means that the site is accessing with a different home_url() than it was registered with. break; case 'site_duplicated': return __('No Vault connection was found for this site (has it moved?); please disconnect and re-connect.', 'updraftplus'); break; } return $message; } /** * This over-rides the method in UpdraftPlus_BackupModule and stops the hidden version field being output. This is so that blank settings are not returned and saved to the database as this storage option outputs no other fields. * * @return [boolean] - return false so that the hidden version field is not output */ public function print_shared_settings_fields() { return false; } /** * Get the pre configuration template * * @return Void - currently does not have a pre config template, this method is needed to stop it taking it's parents */ public function get_pre_configuration_template() { } /** * Get the configuration template * * @return String - the template, ready for substitutions to be carried out */ public function get_configuration_template() { ob_start(); ?> <tr id="remote-storage-updraftvault" class="{{get_template_css_classes true}}"> <th><img id="vaultlogo" src="{{storage_image_url}}" alt="{{method_display_name}}" width="150" height="116"></th> <td valign="top" id="updraftvault_settings_cell"> {{{simplexmlelement_existence_label}}} {{{curl_existence_label}}} <div id="updraftvault_settings_default"{{#if is_connected}} style="display:none;" class="updraft-hidden"{{/if}}> <p> {{{storage_long_description}}} </p> <div class="vault_primary_option clear-left"> <div><strong>{{storage_package_options_label1}}</strong></div> <button aria-label="{{storage_package_options_label1}} {{storage_package_options_label2}}" id="updraftvault_showoptions" class="button-primary">{{storage_package_options_label2}}</button> </div> <div class="vault_primary_option"> <div><strong>{{storage_already_registered_label1}}</strong></div> <button aria-label="{{storage_already_registered_label2}}" id="updraftvault_connect" class="button-primary">{{storage_already_registered_label3}}</button> </div> <p> <em>{{storage_long_description2}}<a target="_blank" href="{{more_vault_info_landing_url}}">{{storage_readmore_label}}</a> <a target="_blank" href="{{more_vault_info_faqs_url}}">{{storage_read_faq_label}}</a></em> </p> </div> <div id="updraftvault_settings_showoptions" style="display:none;" class="updraft-hidden"> <p>{{{storage_package_options_label3}}}</p> <div class="vault-purchase-option-container"> <div class="vault-purchase-option"> <div class="vault-purchase-option-size">5 GB</div> <div class="vault-purchase-option-link"><b>{{price_5gb_package_label}}</b></div> <div class="vault-purchase-option-or">{{start_trial_option_label}}</div> <div class="vault-purchase-option-link"><b>{{discounted_price_5gb_package_label}}</b></div> <div class="vault-purchase-option-link"><a target="_blank" title="{{start_5gb_package_subscription_title}}" href="{{start_5gb_package_subscription_link}}" {{{checkout_embed_5gb_attribute}}}><button aria-label="{{start_trial_button_title}}" class="button-primary">{{start_trial_button_label}}</button></a></div> </div> <div class="vault-purchase-option"> <div class="vault-purchase-option-size">15 GB</div> <div class="vault-purchase-option-link"><b>{{price_15gb_package_label}}</b></div> <div class="vault-purchase-option-or">{{discount_period_label}}</div> <div class="vault-purchase-option-link"><b>{{discounted_price_15gb_package_label}}</b></div> <div class="vault-purchase-option-link"><a target="_blank" title="{{start_15gb_package_subscription_title}}" href="{{start_15gb_package_subscription_link}}" {{{checkout_embed_15gb_attribute}}}><button aria-label="{{start_15gb_subscription_button_title}}" class="button-primary">{{start_subscription_button_label}}</button></a></div> </div> <div class="vault-purchase-option"> <div class="vault-purchase-option-size">50 GB</div> <div class="vault-purchase-option-link"><b>{{price_50gb_package_label}}</b></div> <div class="vault-purchase-option-or">{{discount_period_label}}</div> <div class="vault-purchase-option-link"><b>{{discounted_price_50gb_package_label}}</b></div> <div class="vault-purchase-option-link"><a target="_blank" title="{{start_50gb_package_subscription_title}}" href="{{start_50gb_package_subscription_link}}" {{{checkout_embed_50gb_attribute}}}><button aria-label="{{start_50gb_subscription_button_title}}" class="button-primary">{{start_subscription_button_label}}</button></a></div> </div> <div class="vault-purchase-option"> <div class="vault-purchase-option-size">250 GB</div> <div class="vault-purchase-option-link"><b>{{price_250gb_package_label}}</b></div> <div class="vault-purchase-option-or">{{discount_period_label}}</div> <div class="vault-purchase-option-link"><b>{{discounted_price_250gb_package_label}}</b></div> <div class="vault-purchase-option-link"><a target="_blank" title="{{start_250gb_package_subscription_title}}" href="{{start_250gb_package_subscription_link}}" {{{checkout_embed_250gb_attribute}}}><button aria-label="{{start_250gb_subscription_button_title}}" class="button-primary">{{start_subscription_button_label}}</button></a></div> </div> </div> <p class="clear-left padding-top-20px"> {{subscription_payment_details_label}} </p> <p class="clear-left padding-top-20px"> <em>{{storage_long_description2}} <a target="_blank" href="{{more_vault_info_landing_url}}">{{storage_readmore_label}}</a> <a target="_blank" href="{{more_vault_info_faqs_url}}">{{storage_read_faq_label}}</a></em> </p> <p> <a aria-label="{{go_back_link_label}}" href="{{current_clean_url}}" class="updraftvault_backtostart">{{go_back_link_text}}</a> </p> </div> <div id="updraftvault_settings_connect" data-instance_id="{{instance_id}}" style="display:none;" class="updraft-hidden"> <p>{{connect_to_updraftplus_label}}</p> <p> <input title="{{input_email_title}}" id="updraftvault_email" class="udignorechange" type="text" placeholder="{{input_email_placeholder}}"> <input title="{{input_password_title}}" id="updraftvault_pass" class="udignorechange" type="password" placeholder="{{input_password_placeholder}}"> <button title="{{button_connect_title}}" id="updraftvault_connect_go" class="button-primary">{{button_connect_label}}</button> </p> <p class="padding-top-14px"> <em>{{forgotten_password_label}} <a aria-label="{{forgotten_password_link_label}}" href="{{forgotten_password_link_url}}">{{forgotten_password_link_text}}</a></em> </p> <p class="padding-top-14px"> <em><a aria-label="{{go_back_link_label}}" href="{{current_clean_url}}" class="updraftvault_backtostart">{{go_back_link_text}}</a></em> </p> </div> <div id="updraftvault_settings_connected"{{#unless is_connected}} style="display:none;" class="updraft-hidden"{{/unless}}> {{#if is_connected}} <p id="vault-is-connected">{{{site_is_already_connected_label}}}</p> <p> <strong>{{vault_owner_label}}:</strong> {{email}} <br><strong>{{vault_quota_label}}</strong> {{{quota_text}}} </p> <p><button id="updraftvault_disconnect" class="button-primary">{{button_disconnect_label}}</button></p> {{else}} <p>{{{vault_is_not_connected_label}}}</p> {{/if}} </div> </td> </tr> <?php return ob_get_clean(); } /** * Retrieve a list of template properties by taking all the persistent variables and methods of the parent class and combining them with the ones that are unique to this module, also the necessary HTML element attributes and texts which are also unique only to this backup module * NOTE: Please sanitise all strings that are required to be shown as HTML content on the frontend side (i.e. wp_kses()), or any other technique to prevent XSS attacks that could come via WP hooks * * @return Array an associative array keyed by names that describe themselves as they are */ public function get_template_properties() { global $updraftplus, $updraftplus_admin, $updraftplus_checkout_embed; // Used to decide whether we can afford HTTP calls or not, or would prefer to rely on cached data $this->vault_in_config_print = true; $properties = array( 'storage_image_url' => UPDRAFTPLUS_URL.'/images/updraftvault-150.png', 'simplexmlelement_existence_label' => !apply_filters('updraftplus_vault_simplexmlelement_exists', class_exists('SimpleXMLElement')) ? wp_kses($updraftplus_admin->show_double_warning('<strong>'.__('Warning', 'updraftplus').':</strong> '.sprintf(__("Your web server's PHP installation does not include a <strong>required</strong> (for %s) module (%s).", 'updraftplus'), 'UpdraftVault', 'SimpleXMLElement').' '.__("Please contact your web hosting provider's support and ask for them to enable it.", 'updraftplus'), $this->get_id(), false), $this->allowed_html_for_content_sanitisation()) : '', 'curl_existence_label' => wp_kses($updraftplus_admin->curl_check($updraftplus->backup_methods[$this->get_id()], false, $this->get_id().' hidden-in-updraftcentral', false), $this->allowed_html_for_content_sanitisation()), 'storage_long_description' => wp_kses(__('UpdraftVault brings you storage that is <strong>reliable, easy to use and a great price</strong>.', 'updraftplus').' '.__('Press a button to get started.', 'updraftplus'), $this->allowed_html_for_content_sanitisation()), 'storage_package_options_label1' => __('Need to get space?', 'updraftplus'), 'storage_package_options_label2' => __('Show the options', 'updraftplus'), 'storage_already_registered_label1' => __('Already got space?', 'updraftplus'), 'storage_already_registered_label2' => sprintf(__('Connect to your %s account', 'updraftplus'), $updraftplus->backup_methods[$this->get_id()]), 'storage_already_registered_label3' => __('Connect', 'updraftplus'), 'storage_long_description2' => __("UpdraftVault is built on top of Amazon's world-leading data-centres, with redundant data storage to achieve 99.999999999% reliability.", 'updraftplus'), 'storage_readmore_label' => sprintf(__('Read more about %s here.', 'updraftplus'), $updraftplus->backup_methods[$this->get_id()]), 'storage_read_faq_label' => sprintf(__('Read the %s FAQs here.', 'updraftplus'), 'Vault'), 'more_vault_info_landing_url' => $this->get_url('more_vault_info_landing'), 'more_vault_info_faqs_url' => $this->get_url('more_vault_info_faqs'), 'storage_package_options_label3' => wp_kses(__('UpdraftVault brings you storage that is <strong>reliable, easy to use and a great price</strong>.', 'updraftplus').' '.__('Press a button to get started.', 'updraftplus'), $this->allowed_html_for_content_sanitisation()), 'start_subscription_button_label' => __('Start Subscription', 'updraftplus'), 'start_15gb_subscription_button_title' => sprintf(__('Start %s Subscription', 'updraftplus'), '15GB'), 'start_50gb_subscription_button_title' => sprintf(__('Start %s Subscription', 'updraftplus'), '50GB'), 'start_250gb_subscription_button_title' => sprintf(__('Start %s Subscription', 'updraftplus'), '250GB'), 'start_trial_button_label' => __('Start Trial', 'updraftplus'), 'start_trial_button_title' => sprintf(__('Start %s Trial', 'updraftplus'), '5GB'), 'discount_period_label' => __('or (annual discount)', 'updraftplus'), 'start_trial_option_label' => __('with the option of', 'updraftplus'), 'price_5gb_package_label' => sprintf(__('%s per year', 'updraftplus'), '$35'), 'price_15gb_package_label' => sprintf(__('%s per quarter', 'updraftplus'), '$20'), 'price_50gb_package_label' => sprintf(__('%s per quarter', 'updraftplus'), '$50'), 'price_250gb_package_label' => sprintf(__('%s per quarter', 'updraftplus'), '$125'), 'discounted_price_5gb_package_label' => sprintf(__('%s month %s trial', 'updraftplus'), '1', '$1'), 'discounted_price_15gb_package_label' => sprintf(__('%s per year', 'updraftplus'), '$70'), 'discounted_price_50gb_package_label' => sprintf(__('%s per year', 'updraftplus'), '$175'), 'discounted_price_250gb_package_label' => sprintf(__('%s per year', 'updraftplus'), '$450'), 'start_5gb_package_subscription_title' => sprintf(__('Start a %s UpdraftVault Subscription', 'updraftplus'), '5GB'), 'start_15gb_package_subscription_title' => sprintf(__('Start a %s UpdraftVault Subscription', 'updraftplus'), '15GB'), 'start_50gb_package_subscription_title' => sprintf(__('Start a %s UpdraftVault Subscription', 'updraftplus'), '50GB'), 'start_250gb_package_subscription_title' => sprintf(__('Start a %s UpdraftVault Subscription', 'updraftplus'), '250GB'), 'start_5gb_package_subscription_link' => apply_filters('updraftplus_com_link', $updraftplus->get_url('shop_vault_5')), 'start_15gb_package_subscription_link' => apply_filters('updraftplus_com_link', $updraftplus->get_url('shop_vault_15')), 'start_50gb_package_subscription_link' => apply_filters('updraftplus_com_link', $updraftplus->get_url('shop_vault_50')), 'start_250gb_package_subscription_link' => apply_filters('updraftplus_com_link', $updraftplus->get_url('shop_vault_250')), 'go_back_link_text' => __('Back...', 'updraftplus'), 'go_back_link_label' => sprintf(__('Back to other %s options'), 'Vault'), 'current_clean_url' => UpdraftPlus::get_current_clean_url(), 'subscription_payment_details_label' => __('Payments can be made in US dollars, euros or GB pounds sterling, via card or PayPal.', 'updraftplus').' '. __('Subscriptions can be cancelled at any time.', 'updraftplus'), 'connect_to_updraftplus_label' => __('Enter your UpdraftPlus.Com email / password here to connect:', 'updraftplus'), 'input_email_title' => sprintf(__('Please enter your %s email address', 'updraftplus'), 'UpdraftPlus.com'), 'input_email_placeholder' => __('Email', 'updraftplus'), 'input_password_title' => sprintf(__('Please enter your %s password', 'updraftplus'), 'UpdraftPlus.com'), 'input_password_placeholder' => __('Password', 'updraftplus'), 'button_connect_title' => sprintf(__('Connect to your %s'), 'Vault'), 'button_connect_label' => __('Connect', 'updraftplus'), 'forgotten_password_label' => __("Don't know your email address, or forgotten your password?", 'updraftplus'), 'forgotten_password_link_label' => __("Don't know your email address, or forgotten your password?", 'updraftplus').__('Follow this link for help', 'updraftplus'), 'forgotten_password_link_url' => $this->get_url('vault_forgotten_credentials_links'), 'forgotten_password_link_text' => __('Go here for help', 'updraftplus'), 'site_is_already_connected_label' => wp_kses(__('This site is <strong>connected</strong> to UpdraftVault.', 'updraftplus').' '.__("Well done - there's nothing more needed to set up.", 'updraftplus'), $this->allowed_html_for_content_sanitisation()), 'vault_owner_label' => __('Vault owner', 'updraftplus'), 'vault_quota_label' => __('Quota:', 'updraftplus'), 'button_disconnect_label' => __('Disconnect', 'updraftplus'), 'vault_is_not_connected_label' => wp_kses(__('You are <strong>not connected</strong> to UpdraftVault.', 'updraftplus'), $this->allowed_html_for_content_sanitisation()), ); if ($updraftplus_checkout_embed) { $properties['checkout_embed_5gb_attribute'] = $updraftplus_checkout_embed->get_product('updraftplus-vault-storage-5-gb') ? 'data-embed-checkout="'.esc_attr(apply_filters('updraftplus_com_link', $updraftplus_checkout_embed->get_product('updraftplus-vault-storage-5-gb', UpdraftPlus_Options::admin_page_url().'?page=updraftplus&tab=settings'))).'"' : ''; $properties['checkout_embed_15gb_attribute'] = $updraftplus_checkout_embed->get_product('updraftplus-vault-storage-15-gb') ? 'data-embed-checkout="'.esc_attr(apply_filters('updraftplus_com_link', $updraftplus_checkout_embed->get_product('updraftplus-vault-storage-15-gb', UpdraftPlus_Options::admin_page_url().'?page=updraftplus&tab=settings'))).'"' : ''; $properties['checkout_embed_50gb_attribute'] = $updraftplus_checkout_embed->get_product('updraftplus-vault-storage-50-gb') ? 'data-embed-checkout="'.esc_attr(apply_filters('updraftplus_com_link', $updraftplus_checkout_embed->get_product('updraftplus-vault-storage-50-gb', UpdraftPlus_Options::admin_page_url().'?page=updraftplus&tab=settings'))).'"' : ''; $properties['checkout_embed_250gb_attribute'] = $updraftplus_checkout_embed->get_product('updraftplus-vault-storage-250-gb') ? 'data-embed-checkout="'.esc_attr(apply_filters('updraftplus_com_link', $updraftplus_checkout_embed->get_product('updraftplus-vault-storage-250-gb', UpdraftPlus_Options::admin_page_url().'?page=updraftplus&tab=settings'))).'"' : ''; } $this->vault_in_config_print = false; return wp_parse_args($properties, $this->get_persistent_variables_and_methods()); } /** * Modifies handerbar template options * * @param array $opts * @return Array - Modified handerbar template options */ public function transform_options_for_template($opts) { if (!empty($opts['token']) || !empty($opts['email'])) { $opts['is_connected'] = true; } if (!isset($opts['quota']) || !is_numeric($opts['quota']) || $opts['quota'] < 0) { $opts['quota_text'] = __('Unknown', 'updraftplus'); } else { $opts['quota_text'] = $this->s3_get_quota_info('text', $opts['quota']); } return $opts; } /** * Check whether options have been set up by the user, or not * * @param Array $opts - the potential options * * @return Boolean */ public function options_exist($opts) { if (is_array($opts) && !empty($opts['email'])) return true; return false; } /** * Gives settings keys which values should not passed to handlebarsjs context. * The settings stored in UD in the database sometimes also include internal information that it would be best not to send to the front-end (so that it can't be stolen by a man-in-the-middle attacker) * * @return Array - Settings array keys which should be filtered */ public function filter_frontend_settings_keys() { return array( 'last_config', 'quota', 'quota_root', 'token', ); } private function connected_html($vault_settings = false, $error_message = false) { if (!is_array($vault_settings)) { $vault_settings = $this->get_options(); } if (!is_array($vault_settings) || empty($vault_settings['token']) || empty($vault_settings['email'])) return '<p>'.__('You are <strong>not connected</strong> to UpdraftVault.', 'updraftplus').'</p>'; $ret = '<p id="vault-is-connected">'; $ret .= __('This site is <strong>connected</strong> to UpdraftVault.', 'updraftplus').' '.__("Well done - there's nothing more needed to set up.", 'updraftplus').'</p><p><strong>'.__('Vault owner', 'updraftplus').':</strong> '.htmlspecialchars($vault_settings['email']); $ret .= '<br><strong>'.__('Quota:', 'updraftplus').'</strong> '; if (!isset($vault_settings['quota']) || !is_numeric($vault_settings['quota']) || $vault_settings['quota'] < 0) { if (!$error_message) { $ret .= __('Unknown', 'updraftplus'); $ret .= $this->get_quota_recount_links(); } else { $ret .= $error_message; $ret .= $this->get_quota_recount_links(); } } else { $ret .= $this->s3_get_quota_info('text', $vault_settings['quota']); } $ret .= '</p>'; $ret .= '<p><button id="updraftvault_disconnect" class="button-primary">'.__('Disconnect', 'updraftplus').'</button></p>'; return $ret; } /** * This function will output to the backup log when s3 is out of quota, it will then also clear the vault quota transient so a recount will happen at some point. * * @param Integer $total - the total amount of quota * @param Integer $used - the toal amount used * @param Integer $needed - the amount needed for the upload * * @return void */ protected function s3_out_of_quota($total, $used, $needed) { $quota_transient_used = $this->quota_transient_used ? '(via transient)' : ''; $this->log("Error: Quota exhausted (used=$used, total=$total, needed=$needed) $quota_transient_used"); $this->log(sprintf(__('Error: you have insufficient storage quota available (%s) to upload this archive (%s) (%s).', 'updraftplus'), round(($total-$used)/1048576, 2).' MB', round($needed/1048576, 2).' MB', $quota_transient_used).' '.__('You can get more quota here', 'updraftplus').': '.$this->get_url('get_more_quota'), 'error'); // The transient wasn't intended for 100% precision when that matters (e.g. out-of-quota), so we delete it - a fresh calculation will take place on the next operation delete_transient('updraftvault_quota_numeric'); } /** * This function will setup and record the UpdraftVault quota text transient * * @param Integer $quota_used - the amount of quota used * @param Integer $quota - the total quota * * @return void */ protected function s3_record_quota_info($quota_used, $quota) { $ret = __('Current use:', 'updraftplus').' '.round($quota_used / 1048576, 1).' / '.round($quota / 1048576, 1).' MB'; $ret .= ' ('.sprintf('%.1f', 100*$quota_used / max($quota, 1)).' %)'; $ret .= ' - <a href="'.esc_attr($this->get_url('get_more_quota')).'">'.__('Get more quota', 'updraftplus').'</a>'; $ret_dashboard = $ret . ' - <a href="#" id="updraftvault_recountquota">'.__('Refresh current status', 'updraftplus').'</a>'; set_transient('updraftvault_quota_text', $ret_dashboard, 86400*3); } public function s3_prune_retained_backups_finished() { $config = $this->get_config(); $quota = $config['quota']; $quota_used = $this->s3_get_quota_info('numeric', $config['quota']); $quota_transient_used = $this->quota_transient_used ? ' (via transient)' : ''; $ret = __('Current use:', 'updraftplus').' '.round($quota_used / 1048576, 1).' / '.round($quota / 1048576, 1).' MB'.$quota_transient_used; $ret .= ' ('.sprintf('%.1f', 100*$quota_used / max($quota, 1)).' %)'; $ret_plain = $ret . ' - '.__('Get more quota', 'updraftplus').': '.$this->get_url('get_more_quota'); $ret .= ' - <a href="'.esc_attr($this->get_url('get_more_quota')).'">'.__('Get more quota', 'updraftplus').'</a>'; do_action('updraft_report_remotestorage_extrainfo', 'updraftvault', $ret, $ret_plain); } /** * This function will return the S3 quota Information * * @param String|integer $format n numeric, returns an integer or false for an error (never returns an error) * @param integer $quota S3 quota information * @return String|integer */ protected function s3_get_quota_info($format = 'numeric', $quota = 0) { $ret = ''; $counted = 0; if ($quota > 0) { if (!empty($this->vault_in_config_print) && 'text' == $format) { // See card qwcuddk3 for more info on this; or MR#1175 $quota_via_transient = get_transient('updraftvault_quota_text'); if (is_string($quota_via_transient) && $quota_via_transient) return $quota_via_transient; } elseif ('numeric' == $format) { $quota_via_transient = get_transient('updraftvault_quota_numeric'); if (is_numeric($quota_via_transient) && $quota_via_transient && round($quota - $quota_via_transient, 1048576) >= 1024) { $this->quota_transient_used = true; if (!defined('UPDRAFTVAULT_COUNT_QUOTA_ANYWAY') || !UPDRAFTVAULT_COUNT_QUOTA_ANYWAY) { return $quota_via_transient; } } else { $this->quota_transient_used = false; } } try { $config = $this->get_config(); if (empty($config['quota_root'])) { // This next line is wrong: it lists the files *in this site's sub-folder*, rather than the whole Vault $current_files = $this->listfiles(''); } else { $current_files = $this->listfiles_with_path($config['quota_root'], '', true); } } catch (Exception $e) { $this->log("Listfiles failed during quota calculation: ".$e->getMessage()); $current_files = new WP_Error('listfiles_exception', $e->getMessage().' ('.get_class($e).')'); } $ret .= __('Current use:', 'updraftplus').' '; if (is_wp_error($current_files)) { $ret .= __('Error:', 'updraftplus').' '.$current_files->get_error_message().' ('.$current_files->get_error_code().')'; } elseif (!is_array($current_files)) { $ret .= __('Unknown', 'updraftplus'); } else { foreach ($current_files as $file) { $counted += $file['size']; } if ($this->quota_transient_used && defined('UPDRAFTVAULT_COUNT_QUOTA_ANYWAY') && UPDRAFTVAULT_COUNT_QUOTA_ANYWAY) { $this->log("UpdraftVault: UPDRAFTVAULT_COUNT_QUOTA_ANYWAY set. Current quota: {$counted}"); } else { set_transient('updraftvault_quota_numeric', $counted, 86400); } $ret .= round($counted / 1048576, 1); $ret .= ' / '.round($quota / 1048576, 1).' MB'; $ret .= ' ('.sprintf('%.1f', 100*$counted / $quota).' %)'; } } else { $ret .= '0'; } $ret .= $this->get_quota_recount_links(); if ('text' == $format) set_transient('updraftvault_quota_text', $ret, 86400*3); return ('text' == $format) ? $ret : $counted; } /** * Build the links to recount used vault quota and to purchase more quota * * @return String */ private function get_quota_recount_links() { return ' - <a href="'.esc_attr($this->get_url('get_more_quota')).'">'.__('Get more quota', 'updraftplus').'</a> - <a href="'.esc_url(UpdraftPlus::get_current_clean_url()).'" id="updraftvault_recountquota">'.__('Refresh current status', 'updraftplus').'</a>'; } public function ajax_vault_recountquota($echo_results = true) { // Force the opts to be refreshed $config = $this->get_config(); if (empty($config['accesskey']) && !empty($config['error_message'])) { if (!empty($config['error']) && is_array($config['error']) && 'fetch_credentials_error' == $config['error']['message']) { $opts = array('token' => 'unknown', 'email' => $config['email'], 'quota' => -1); $results = array('html' => $this->connected_html($opts, $config['error_message']), 'connected' => 1); } else { $results = array('html' => htmlspecialchars($config['error_message']), 'connected' => 0); } } else { // Now read the opts $opts = $this->get_options(); $results = array('html' => $this->connected_html($opts), 'connected' => 1); } if ($echo_results) { echo json_encode($results); } else { return $results; } } /** * This method also gets called directly, so don't add code that assumes that it's definitely an AJAX situation * * @param Boolean $echo_results check to see if the results need to be echoed * @return Array */ public function ajax_vault_disconnect($echo_results = true) { $vault_settings = $this->get_options(); $frontend_settings_keys = array_flip($this->filter_frontend_settings_keys()); foreach ((array) $frontend_settings_keys as $key => $val) { $frontend_settings_keys[$key] = ('last_config' === $key) ? array() : ''; } $this->set_options(array_merge($frontend_settings_keys, $this->get_default_options()), true); global $updraftplus; delete_transient('udvault_last_config'); delete_transient('updraftvault_quota_text'); $response = array('disconnected' => 1, 'html' => $this->connected_html()); if ($echo_results) { $updraftplus->close_browser_connection(json_encode($response)); } // If $_POST['reset_hash'] is set, then we were alerted by updraftplus.com - no need to notify back if (is_array($vault_settings) && isset($vault_settings['email']) && empty($_POST['reset_hash'])) { $post_body = array( 'e' => (string) $vault_settings['email'], 'sid' => $updraftplus->siteid(), 'su' => base64_encode(home_url()) ); if (!empty($vault_settings['token'])) $post_body['token'] = (string) $vault_settings['token']; // Use SSL to prevent snooping wp_remote_post($this->vault_mothership.'/?udm_action=vault_disconnect', array( 'timeout' => 20, 'body' => $post_body, )); } return $response; } /** * This is called from the UD admin object * * @param Boolean $echo_results A Flag to see if results need to be echoed or returned * @param Boolean|array $use_credentials Check if Vault needs to use credentials * @return Array */ public function ajax_vault_connect($echo_results = true, $use_credentials = false) { if (empty($use_credentials)) $use_credentials = $_REQUEST; $connect = $this->vault_connect($use_credentials['email'], $use_credentials['pass']); if (true === $connect) { $response = array('connected' => true, 'html' => $this->connected_html(false)); } else { $response = array( 'e' => __('An unknown error occurred when trying to connect to UpdraftPlus.Com', 'updraftplus') ); if (is_wp_error($connect)) { $response['e'] = $connect->get_error_message(); $response['code'] = $connect->get_error_code(); $response['data'] = serialize($connect->get_error_data()); } } if ($echo_results) { echo json_encode($response); } else { return $response; } } /** * Returns either true (in which case the Vault token will be stored), or false|WP_Error * * @param String $email Vault Email * @param String $password Vault Password * @return Boolean|WP_Error */ private function vault_connect($email, $password) { // Username and password set up? if (empty($email) || empty($password)) return new WP_Error('blank_details', __('You need to supply both an email address and a password', 'updraftplus')); global $updraftplus; $remote_post_array = apply_filters('updraftplus_vault_config_add_headers', array( 'timeout' => 20, 'body' => array( 'e' => $email, 'p' => base64_encode($password), 'sid' => $updraftplus->siteid(), 'su' => base64_encode(home_url()), 'v' => $updraftplus->version ) )); // Use SSL to prevent snooping $result = wp_remote_post($this->vault_mothership.'/?udm_action=vault_connect', $remote_post_array); if (is_wp_error($result) || false === $result) return $result; $response = json_decode(wp_remote_retrieve_body($result), true); if (!is_array($response) || !isset($response['mothership']) || !isset($response['loggedin'])) { if (preg_match('/has banned your IP address \(([\.:0-9a-f]+)\)/', $result['body'], $matches)) { return new WP_Error('banned_ip', sprintf(__("UpdraftPlus.com has responded with 'Access Denied'.", 'updraftplus').'<br>'.__("It appears that your web server's IP Address (%s) is blocked.", 'updraftplus').' '.__('This most likely means that you share a webserver with a hacked website that has been used in previous attacks.', 'updraftplus').'<br> <a href="'.apply_filters("updraftplus_com_link", "https://updraftplus.com/unblock-ip-address/").'" target="_blank">'.__('To remove the block, please go here.', 'updraftplus').'</a> ', $matches[1])); } else { return new WP_Error('unknown_response', sprintf(__('UpdraftPlus.Com returned a response which we could not understand (data: %s)', 'updraftplus'), wp_remote_retrieve_body($result))); } } switch ($response['loggedin']) { case 'connected': if (!empty($response['token'])) { // Store it $vault_settings = $this->get_options(); if (!is_array($vault_settings)) $vault_settings = array(); $vault_settings['email'] = $email; $vault_settings['token'] = (string) $response['token']; $vault_settings['quota'] = -1; unset($vault_settings['last_config']); if (isset($response['quota'])) $vault_settings['quota'] = $response['quota']; $this->set_options($vault_settings, true); if (!empty($response['config']) && is_array($response['config'])) { if (!empty($response['config']['accesskey'])) { $this->vault_set_config($response['config']); } elseif (!empty($response['config']['result']) && ('token_unknown' == $response['config']['result'] || 'site_duplicated' == $response['config']['result'])) { return new WP_Error($response['config']['result'], $this->vault_translate_remote_message($response['config']['message'], $response['config']['result'])); } // else... would also be an error condition, but not one known possible (and it will show a generic error anyway) } } elseif (isset($response['quota']) && !$response['quota']) { return new WP_Error('no_quota', __('You do not currently have any UpdraftVault quota', 'updraftplus')); } else { return new WP_Error('unknown_response', __('UpdraftPlus.Com returned a response, but we could not understand it', 'updraftplus')); } break; case 'authfailed': if (!empty($response['authproblem'])) { if ('invalidpassword' == $response['authproblem']) { $authfail_error = new WP_Error('authfailed', __('Your email address was valid, but your password was not recognised by UpdraftPlus.Com.', 'updraftplus').' <a href="'.esc_attr($this->get_url('vault_forgotten_credentials_links')).'">'.__('If you have forgotten your password, then go here to change your password on updraftplus.com.', 'updraftplus').'</a>'); return $authfail_error; } elseif ('invaliduser' == $response['authproblem']) { return new WP_Error('authfailed', __('You entered an email address that was not recognised by UpdraftPlus.Com', 'updraftplus')); } } return new WP_Error('authfailed', __('Your email address and password were not recognised by UpdraftPlus.Com', 'updraftplus')); break; case 'iamfailed': if (!empty($response['authproblem'])) { if ('gettempcreds_exception2' == $response['authproblem'] || 'gettempcreds_exception2' == $response['authproblem']) { $authfail_error = new WP_Error('authfailed', __('An error occurred while fetching your Vault credentials.', 'updraftplus').' '.__('Please try again after a few minutes.')); } else { $authfail_error = new WP_Error('authfailed', __('An unknown error occurred while connecting to Vault.', 'updraftplus').' '.__('Please try again.')); } return $authfail_error; } return new WP_Error('unknown_response', __('UpdraftPlus.Com returned a response, but we could not understand it', 'updraftplus')); break; default: return new WP_Error('unknown_response', __('UpdraftPlus.Com returned a response, but we could not understand it', 'updraftplus')); break; } return true; } /** * Acts as a WordPress options filter * * @param Array $updraftvault - An array of UpdraftVault options * @return Array - the set of updated UpdraftVault settings */ public function options_filter($updraftvault) { // Get the current options (and possibly update them to the new format) $opts = UpdraftPlus_Storage_Methods_Interface::update_remote_storage_options_format('updraftvault'); if (is_wp_error($opts)) { if ('recursion' !== $opts->get_error_code()) { $msg = "(".$opts->get_error_code()."): ".$opts->get_error_message(); $this->log($msg); error_log("UpdraftPlus: $msg"); } // The saved options had a problem; so, return the new ones return $updraftvault; } // If the input is either empty or not as expected, then return the current options if (!isset($updraftvault['settings']) || !is_array($updraftvault['settings']) || empty($updraftvault['settings'])) return $opts; foreach ($updraftvault['settings'] as $instance_id => $storage_options) { if (!isset($opts['settings'][$instance_id])) continue; foreach ($storage_options as $storage_key => $storage_value) { $opts['settings'][$instance_id][$storage_key] = $storage_value; } } return $opts; } /** * Set region that was recieved by previously performing location detection (i.e. getBucketLocation) and set the endpoint by concatenating the service hostname of the provider in use and the region * * @param Object $obj Storage object * @param String $region bucket location * @param String $bucket_name bucket name */ protected function set_region($obj, $region = '', $bucket_name = '') { $config = $this->get_config(); if (isset($config['provider']) && 'wasabi' == $config['provider']) { // https://knowledgebase.wasabi.com/hc/en-us/articles/360015106031-What-are-the-service-URLs-for-Wasabi-s-different-storage-regions $endpoint = ''; switch ($region) { case 'US': $endpoint = 's3.wasabisys.com'; $region = 'us-east-1'; break; case 'us-east-1': case 'us-east-2': case 'ap-southeast-1': case 'ap-southeast-2': case 'ap-northeast-1': case 'ap-northeast-2': case 'eu-west-1': case 'eu-west-2': case 'eu-central-1': case 'eu-central-2': case 'ca-central-1': case 'us-west-1': case 'us-central-1': $endpoint = 's3.'.$region.'.wasabisys.com'; break; default: break; } if ($endpoint) { $this->log("Set region (".get_class($obj)."): $region"); $obj->setRegion($region); if (!is_a($obj, 'UpdraftPlus_S3_Compat')) { $this->log("Set endpoint: $endpoint"); $obj->setEndpoint($endpoint); } } } else { // the default AWS provider is in use, so we set region using mechanism defined for the AWS in the parent class parent::set_region($obj, $region, $bucket_name); } } /** * Get an S3 object by specifying the global endpoint of the provider being used * * @param String $key S3 Key * @param String $secret S3 secret * @param Boolean $useservercerts User server certificates * @param Boolean $disableverify Check if disableverify is enabled * @param Boolean $nossl Check if there is SSL or not * @param Null|String $endpoint S3 endpoint to use * @param Boolean $sse A flag to use server side encryption * @param String $session_token The session token returned by AWS for temporary credentials access * * @return Object|WP_Error */ public function getS3($key, $secret, $useservercerts, $disableverify, $nossl, $endpoint = null, $sse = false, $session_token = null) { $config = $this->get_config(); if (isset($config['provider']) && 'wasabi' == $config['provider']) { // UpdraftPlus_BackupModule_s3 is abstract and by default linked to S3 AWS provider, the same with Vault which is the descendant class of UpdraftPlus_BackupModule_s3 which also uses S3 AWS by default // but since Vault now supports Wasabi provider and Wasabi is a provider that also has regions, we override and choose not to pass the "s3.wasabisys.com" endpoint directly to every method that calls getS3() in the UpdraftPlus_BackupModule_s3 class to prevent unnecessary checks being done in the abstract layer return parent::getS3($key, $secret, $useservercerts, $disableverify, $nossl, 's3.wasabisys.com', $sse, $session_token); } else { return parent::getS3($key, $secret, $useservercerts, $disableverify, $nossl, $endpoint, $sse, $session_token); } } } webdav.php 0000644 00000002142 15231511727 0006530 0 ustar 00 <?php if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed.'); if (class_exists('UpdraftPlus_Addons_RemoteStorage_webdav')) { // Migrate options to new-style storage - April 2017 if (!is_array(UpdraftPlus_Options::get_updraft_option('updraft_webdav')) && '' != UpdraftPlus_Options::get_updraft_option('updraft_webdav_settings', '')) { $opts = UpdraftPlus_Options::get_updraft_option('updraft_webdav_settings'); UpdraftPlus_Options::update_updraft_option('updraft_webdav', $opts); UpdraftPlus_Options::delete_updraft_option('updraft_webdav_settings'); } class UpdraftPlus_BackupModule_webdav extends UpdraftPlus_Addons_RemoteStorage_webdav { public function __construct() { parent::__construct('webdav', 'WebDAV'); } } } else { updraft_try_include_file('methods/addon-not-yet-present.php', 'include_once'); /** * N.B. UpdraftPlus_BackupModule_AddonNotYetPresent extends UpdraftPlus_BackupModule */ class UpdraftPlus_BackupModule_webdav extends UpdraftPlus_BackupModule_AddonNotYetPresent { public function __construct() { parent::__construct('webdav', 'WebDAV'); } } } s3generic.php 0000644 00000037243 15231511727 0007154 0 ustar 00 <?php if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed.'); updraft_try_include_file('methods/s3.php', 'require_once'); /** * Converted to multi-options (Feb 2017-) and previous options conversion removed: Yes */ class UpdraftPlus_BackupModule_s3generic extends UpdraftPlus_BackupModule_s3 { protected $provider_can_use_aws_sdk = false; protected $provider_has_regions = false; /** * Given an S3 object, possibly set the region on it * * @param Object $obj - like UpdraftPlus_S3 * @param String $region * @param String $bucket_name */ protected function set_region($obj, $region = '', $bucket_name = '') { $config = $this->get_config(); $endpoint = ('' != $region && 'n/a' != $region) ? $region : $config['endpoint']; if (!empty($endpoint)) { $endpoint = preg_replace('/^(http|https):\/\//i', '', trim($endpoint)); } $log_message = "Set endpoint (".get_class($obj)."): $endpoint"; $log_message_append = ''; if (is_string($endpoint) && preg_match('/^(.*):(\d+)$/', $endpoint, $matches)) { $endpoint = $matches[1]; $port = $matches[2]; $log_message_append = ", port=$port"; $obj->setPort($port); } // This provider requires domain-style access. In future it might be better to provide an option rather than hard-coding the knowledge. if (is_string($endpoint) && preg_match('/\.aliyuncs\.com$/i', $endpoint)) { $obj->useDNSBucketName(true, $bucket_name); } global $updraftplus; if ($updraftplus->backup_time) $this->log($log_message.$log_message_append); $obj->setEndpoint($endpoint); } /** * This method overrides the parent method and lists the supported features of this remote storage option. * * @return Array - an array of supported features (any features not mentioned are asuumed to not be supported) */ public function get_supported_features() { // This options format is handled via only accessing options via $this->get_options() return array('multi_options', 'config_templates', 'multi_storage', 'conditional_logic'); } /** * Retrieve default options for this remote storage module. * * @return Array - an array of options */ public function get_default_options() { return array( 'accesskey' => '', 'secretkey' => '', 'path' => '', 'endpoint' => '', ); } /** * Retrieve specific options for this remote storage module * * @param Boolean $force_refresh - if set, and if relevant, don't use cached credentials, but get them afresh * * @return Array - an array of options */ protected function get_config($force_refresh = false) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Filter use $opts = $this->get_options(); $opts['whoweare'] = 'S3'; $opts['whoweare_long'] = __('S3 (Compatible)', 'updraftplus'); $opts['key'] = 's3generic'; return $opts; } /** * Get the pre configuration template */ public function get_pre_configuration_template() { ?> <tr class="{{get_template_css_classes false}} S3_pre_config_container"> <td colspan="2"> {{{pre_template_opening_html}}} <br> {{{xmlwriter_existence_label}}} {{{simplexmlelement_existence_label}}} {{{curl_existence_label}}} <br> <p> {{{ssl_certificates_errors_link_text}}} </p> </td> </tr> <?php } /** * Get partial templates of the S3-Generic remote storage, the partial template is recognised by its name. To find out a name of partial template, look for the partial call syntax in the template, it's enclosed by double curly braces (i.e. {{> partial_template_name }}) * * @return Array an associative array keyed by name of the partial templates */ public function get_partial_templates() { $partial_templates = array(); $partial_templates['s3generic_additional_configuration_top'] = ''; ob_start(); ?> <tr class="{{get_template_css_classes true}}"> <th>{{input_endpoint_label}}:</th> <td> <input data-updraft_settings_test="endpoint" type="text" class="updraft_input--wide udc-wd-600" id="{{get_template_input_attribute_value "id" "endpoint"}}" name="{{get_template_input_attribute_value "name" "endpoint"}}" value="{{endpoint}}" /> </td> </tr> <tr class="{{get_template_css_classes true}}"> <th>{{input_bucket_access_style_label}}:<br>{{{input_bucket_access_style_readmore}}}</th> <td> <select data-updraft_settings_test="bucket_access_style" id="{{get_template_input_attribute_value "id" "bucket_access_style"}}" name="{{get_template_input_attribute_value "name" "bucket_access_style"}}" class="udc-wd-600"> {{#each input_bucket_access_style_option_labels}} <option {{#ifeq ../bucket_access_style @key}}selected="selected"{{/ifeq}} value="{{@key}}">{{this}}</option> {{/each}} </select> </td> </tr> <tr class="{{get_template_css_classes true}}"> <th>{{input_signature_version_label}}:<br>{{{input_signature_version_readmore}}}</th> <td> <select data-updraft_settings_test="signature_version" id="{{get_template_input_attribute_value "id" "signature_version"}}" name="{{get_template_input_attribute_value "name" "signature_version"}}" class="udc-wd-600"> {{#each input_signature_version_option_labels}} <option {{#ifeq ../signature_version @key}}selected="selected"{{/ifeq}} value="{{@key}}">{{this}}</option> {{/each}} </select> </td> </tr> <?php $partial_templates['s3generic_additional_configuration_bottom'] = ob_get_clean(); return wp_parse_args(apply_filters('updraft_'.$this->get_id().'_partial_templates', $partial_templates), parent::get_partial_templates()); } /** * Modifies handerbar template options * The function require because It should override parent class's UpdraftPlus_BackupModule_s3::transform_options_for_template() functionality with no operation. * * @param array $opts * @return Array - Modified handerbar template options */ public function transform_options_for_template($opts) { if (!empty($opts['instance_id']) && 'default' !== $opts['instance_id']) { if (!isset($opts['signature_version'])) { // this check is to find out whether we're dealing with a pre-existing configuration or not $opts['signature_version'] = 'v2'; // the pre-existing S3-Compatible configurations before signature_version was introduced by default use SigV2, so we wan't to keep it that way as we don't want to break what's already working $opts['signature_version'] = apply_filters('updraftplus_s3_signature_version', $opts['signature_version'], false, $this); if (!empty($opts['endpoint'])) { if (preg_match('/\.(leviia|r2\.cloudflarestorage)\.com$/i', $opts['endpoint']) || (preg_match('/\.amazonaws\.com$/i', $opts['endpoint']) && !empty($opts['bucket_access_style']) && 'virtual_host_style' === $opts['bucket_access_style'])) { // due to the merge of S3-generic bucket access style MR on March 2021, if virtual-host bucket access style is selected, connecting to an amazonaws bucket location where the user doesn't have an access to it will throw an S3 InvalidRequest exception. It requires the signature to be set to version 4 $opts['signature_version'] = 'v4'; } } } if (!$this->options_exist($opts)) $opts['signature_version'] = 'v4'; // if no pre-existing S3-Compatible configurations were setup, there would always be an initial instance id created with a blank configuration form (no access key, secret key, location, and endpoint), this initial instance id/form should use SigV4 } return $opts; } /** * Check whether options have been set up by the user, or not * * @param Array $opts - the potential options * * @return Boolean */ public function options_exist($opts) { return (parent::options_exist($opts) && !empty($opts['endpoint'])); } /** * Retrieve a list of template properties by taking all the persistent variables and methods of the parent class and combining them with the ones that are unique to this module, also the necessary HTML element attributes and texts which are also unique only to this backup module * NOTE: Please sanitise all strings that are required to be shown as HTML content on the frontend side (i.e. wp_kses()), or any other technique to prevent XSS attacks that could come via WP hooks * * @return Array an associative array keyed by names that describe themselves as they are */ public function get_template_properties() { global $updraftplus, $updraftplus_admin; $properties = array( 'pre_template_opening_html' => wp_kses('<p>'.__('Examples of S3-compatible storage providers:', 'updraftplus').' <a href="https://updraftplus.com/use-updraftplus-digital-ocean-spaces/" target="_blank">DigitalOcean Spaces</a>, <a href="https://www.linode.com/products/object-storage/" target="_blank">Linode Object Storage</a>, <a href="https://www.cloudian.com" target="_blank">Cloudian</a>, <a href="https://www.mh.connectria.com/rp/order/cloud_storage_index" target="_blank">Connectria</a>, <a href="https://www.constant.com/cloud/storage/" target="_blank">Constant</a>, <a href="https://www.eucalyptus.cloud/" target="_blank">Eucalyptus</a>, <a href="http://cloud.nifty.com/storage/" target="_blank">Nifty</a>, <a href="http://www.ntt.com/business/services/cloud/iaas/cloudn.html" target="_blank">Cloudn</a>'.__('... and many more!', 'updraftplus').'</p>', $this->allowed_html_for_content_sanitisation()), 'xmlwriter_existence_label' => !apply_filters('updraftplus_s3generic_xmlwriter_exists', 'UpdraftPlus_S3_Compat' != $this->indicate_s3_class() || !class_exists('XMLWriter')) ? wp_kses($updraftplus_admin->show_double_warning('<strong>'.__('Warning', 'updraftplus').':</strong> '.sprintf(__("Your web server's PHP installation does not include a required module (%s).", 'updraftplus'), 'XMLWriter').' '.__("Please contact your web hosting provider's support and ask for them to enable it.", 'updraftplus'), $this->get_id(), false), $this->allowed_html_for_content_sanitisation()) : '', 'simplexmlelement_existence_label' => !apply_filters('updraftplus_s3generic_simplexmlelement_exists', class_exists('SimpleXMLElement')) ? wp_kses($updraftplus_admin->show_double_warning('<strong>'.__('Warning', 'updraftplus').':</strong> '.sprintf(__("Your web server's PHP installation does not include a required module (%s).", 'updraftplus'), 'SimpleXMLElement').' '.__("Please contact your web hosting provider's support.", 'updraftplus').' '.sprintf(__("UpdraftPlus's %s module <strong>requires</strong> %s.", 'updraftplus'), $updraftplus->backup_methods[$this->get_id()], 'SimpleXMLElement').' '.__('Please do not file any support requests; there is no alternative.', 'updraftplus'), $this->get_id(), false), $this->allowed_html_for_content_sanitisation()) : '', 'curl_existence_label' => wp_kses($updraftplus_admin->curl_check($updraftplus->backup_methods[$this->get_id()], true, $this->get_id().' hide-in-udc', false), $this->allowed_html_for_content_sanitisation()), 'ssl_certificates_errors_link_text' => wp_kses('<a href="'.apply_filters("updraftplus_com_link", "https://updraftplus.com/faqs/i-get-ssl-certificate-errors-when-backing-up-andor-restoring/").'" target="_blank">'.__('If you see errors about SSL certificates, then please go here for help.', 'updraftplus').'</a>', $this->allowed_html_for_content_sanitisation()), 'input_access_key_label' => sprintf(__('%s access key', 'updraftplus'), 'S3'), 'input_secret_key_label' => sprintf(__('%s secret key', 'updraftplus'), 'S3'), 'input_secret_key_type' => apply_filters('updraftplus_admin_secret_field_type', 'password'), 'input_location_label' => sprintf(__('%s location', 'updraftplus'), 'S3'), 'input_location_title' => __('Enter only a bucket name or a bucket and path.', 'updraftplus').' '.__('Examples: mybucket, mybucket/mypath', 'updraftplus'), 'input_endpoint_label' => sprintf(__('%s end-point', 'updraftplus'), 'S3'), 'input_bucket_access_style_label' => __('Bucket access style', 'updraftplus'), 'input_bucket_access_style_readmore' => wp_kses('<a aria-label="'.esc_attr__('Read more about bucket access style', 'updraftplus').'" href="https://updraftplus.com/faqs/what-is-the-different-between-path-style-and-bucket-style-access-to-an-s3-compatible-bucket/" target="_blank"><em>'.__('(Read more)', 'updraftplus').'</em></a>', $this->allowed_html_for_content_sanitisation()), 'input_bucket_access_style_option_labels' => array( 'path_style' => __('Path style', 'updraftplus'), 'virtual_host_style' => __('Virtual-host style', 'updraftplus'), ), 'input_signature_version_label' => __('Signature version', 'updraftplus'), 'input_signature_version_readmore' => wp_kses('<a aria-label="'.esc_attr__('Read more about signature version', 'updraftplus').'" href="https://aws.amazon.com/blogs/aws/amazon-s3-update-sigv2-deprecation-period-extended-modified/" target="_blank"><em>'.__('(Read more)', 'updraftplus').'</em></a>', $this->allowed_html_for_content_sanitisation()), 'input_signature_version_option_labels' => array( 'v4' => __('SigV4', 'updraftplus'), 'v2' => __('SigV2', 'updraftplus'), ), 'input_test_label' => sprintf(__('Test %s Settings', 'updraftplus'), $updraftplus->backup_methods[$this->get_id()]), ); return wp_parse_args($properties, $this->get_persistent_variables_and_methods()); } /** * Use DNS bucket name if the remote storage is found to be using s3generic and its bucket access style is set to virtual-host * * @param Object $storage - S3 Name * @param String $bucket - storage path * @param Array $config - configuration - may not be complete at this stage, so be careful about which properties are used * * * @return Boolean true if currently processing s3generic remote storage that uses virtual-host style, false otherwise */ protected function maybe_use_dns_bucket_name($storage, $bucket, $config) { $signature_version = !empty($config['signature_version']) ? $config['signature_version'] : apply_filters('updraftplus_s3_signature_version', 'v2', false, $this); // the 'v2' value used to be handled by the use_v4 class variable, but since the signature_version (dropdown) setting has been introduced then use_v4 class variable has been removed and is no longer needed if (is_callable(array($storage, 'setSignatureVersion'))) $storage->setSignatureVersion($signature_version); // we don't prioritise the hardcoded endpoint if signature_version is set, which means users are aware of this new option and intentionally set this option and/or save the settings if ((!empty($config['endpoint']) && preg_match('/\.(leviia|aliyuncs|r2\.cloudflarestorage)\.com$/i', $config['endpoint'])) || (!empty($config['bucket_access_style']) && 'virtual_host_style' === $config['bucket_access_style'])) { // due to the recent merge of S3-generic bucket access style on March 2021, if virtual-host bucket access style is selected, connecting to an amazonaws bucket location where the user doesn't have an access to it will throw an S3 InvalidRequest exception. It requires the signature to be set to version 4 // Cloudflare R2 supports V4 only if (empty($config['signature_version']) && preg_match('/\.(leviia|amazonaws|r2\.cloudflarestorage)\.com$/i', $config['endpoint'])) { if (is_callable(array($storage, 'setSignatureVersion'))) $storage->setSignatureVersion('v4'); } return $this->use_dns_bucket_name($storage, ''); } return false; } /** * Acts as a WordPress options filter * * @param Array $settings - pre-filtered settings * * @return Array filtered settings */ public function options_filter($settings) { $settings = parent::options_filter($settings); if (!empty($settings['version']) && !empty($settings['settings'])) { foreach ($settings['settings'] as $instance_id => $instance_settings) { if (!empty($instance_settings['endpoint'])) { $settings['settings'][$instance_id]['endpoint'] = preg_replace('/^(http|https):\/\//i', '', trim($instance_settings['endpoint'])); } } } return $settings; } } insufficient.php 0000644 00000011767 15231511727 0007763 0 ustar 00 <?php if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed.'); if (!class_exists('UpdraftPlus_BackupModule')) updraft_try_include_file('methods/backup-module.php', 'require_once'); class UpdraftPlus_BackupModule_insufficientphp extends UpdraftPlus_BackupModule { private $required_php; private $error_msg; private $method; private $desc; private $image; private $error_msg_trans; public function __construct($method, $desc, $php, $image = null) { $this->method = $method; $this->desc = $desc; $this->required_php = $php; $this->image = $image; $this->error_msg = 'This remote storage method ('.$this->desc.') requires PHP '.$this->required_php.' or later'; $this->error_msg_trans = sprintf(__('This remote storage method (%s) requires PHP %s or later.', 'updraftplus'), $this->desc, $this->required_php); } private function log_error() { global $updraftplus; $updraftplus->log($this->error_msg); $updraftplus->log($this->error_msg_trans, 'error', 'insufficientphp'); return false; } /** * backup method: takes an array, and shovels them off to the cloud storage * * @param array $backup_array An array backups * @return Array */ public function backup($backup_array) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Unused variable is present because the function to perform backup for specific storage is not exist. return $this->log_error(); } /** * Retrieve a list of supported features for this storage method * * Currently known features: * * - multi_options : indicates that the remote storage module * can handle its options being in the Feb-2017 multi-options * format. N.B. This only indicates options handling, not any * other multi-destination options. * * - multi_servers : not implemented yet: indicates that the * remote storage module can handle multiple servers at backup * time. This should not be specified without multi_options. * multi_options without multi_servers is fine - it will just * cause only the first entry in the options array to be used. * * - config_templates : not implemented yet: indicates that * the remote storage module can output its configuration in * Handlebars format via the get_configuration_template() method. * * @return Array - an array of supported features (any features not * mentioned are assumed to not be supported) */ public function get_supported_features() { // The 'multi_options' options format is handled via only accessing options via $this->get_options() return array('multi_options', 'config_templates'); } /** * $match: a substring to require (tested via strpos() !== false) * * @param String $match THis will specify which match is used for the SQL but by default it is set to 'backup_' unless specified * @return Array */ public function listfiles($match = 'backup_') {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Unused variable is present because the function to perform listfiles for specific storage is not exist. return new WP_Error('insufficient_php', $this->error_msg_trans); } /** * delete method: takes an array of file names (base name) or a single string, and removes them from the cloud storage * * @param String $files List of files * @param Boolean $data Specifies data or not * @param array $sizeinfo This is the size info on the file. * @return Array */ public function delete($files, $data = false, $sizeinfo = array()) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Unused variable is present because the function to perform delete for specific storage is not exist. return $this->log_error(); } /** * download method: takes a file name (base name), and brings it back from the cloud storage into Updraft's directory * You can register errors with $updraftplus->log("my error message", 'error') * * @param String $file List of files * @return Array */ public function download($file) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Unused variable is present because the function to perform download for specific storage is not exist. return $this->log_error(); } private function extra_config() { } /** * Get the configuration template * * @return String - the template, ready for substitutions to be carried out */ public function get_configuration_template() { ob_start(); $this->extra_config(); ?> <tr class="updraftplusmethod <?php echo esc_attr($this->method);?>"> <th><?php echo esc_html($this->desc);?>:</th> <td> <em> <?php echo (!empty($this->image)) ? '<p><img src="'.esc_url(UPDRAFTPLUS_URL.'/images/'.$this->image).'"></p>' : ''; ?> <?php echo esc_html($this->error_msg_trans);?> <?php esc_html_e('You will need to ask your web hosting company to upgrade.', 'updraftplus');?> <?php echo esc_html(sprintf(__('Your %s version: %s.', 'updraftplus'), 'PHP', phpversion()));?> </em> </td> </tr> <?php return ob_get_clean(); } } googlecloud.php 0000644 00000002264 15231511727 0007570 0 ustar 00 <?php if (!defined('UPDRAFTPLUS_DIR')) die('No direct access.'); if (class_exists('UpdraftPlus_BackupModule_googlecloud')) return; if (version_compare(PHP_VERSION, '5.2.4', '>=')) { if (class_exists('UpdraftPlus_Addons_RemoteStorage_googlecloud')) { class UpdraftPlus_BackupModule_googlecloud extends UpdraftPlus_Addons_RemoteStorage_googlecloud { public function __construct() { parent::__construct('googlecloud', 'Google Cloud', '5.2.4', 'googlecloud.png'); } } } else { updraft_try_include_file('methods/addon-not-yet-present.php', 'include_once'); /** * N.B. UpdraftPlus_BackupModule_AddonNotYetPresent extends UpdraftPlus_BackupModule */ class UpdraftPlus_BackupModule_googlecloud extends UpdraftPlus_BackupModule_AddonNotYetPresent { public function __construct() { parent::__construct('googlecloud', 'Google Cloud', '5.2.4', 'googlecloud.png'); } } } } else { updraft_try_include_file('methods/insufficient.php', 'include_once'); class UpdraftPlus_BackupModule_googlecloud extends UpdraftPlus_BackupModule_insufficientphp { public function __construct() { parent::__construct('googlecloud', 'Google Cloud', '5.2.4', 'googlecloud.png'); } } } cloudfiles.php 0000644 00000055560 15231511727 0007425 0 ustar 00 <?php if (!defined('UPDRAFTPLUS_DIR')) die('No direct access.'); /** * Converted to job_options: yes * Converted to array options: yes * Migration code for "new"-style options removed: Feb 2017 (created: Dec 2013) */ if (!class_exists('UpdraftPlus_BackupModule')) updraft_try_include_file('methods/backup-module.php', 'require_once'); /** * Old SDK */ class UpdraftPlus_BackupModule_cloudfiles_oldsdk extends UpdraftPlus_BackupModule { /** * This function does not catch any exceptions - that should be done by the caller * * @param String $user * @param String $apikey * @param String $authurl * @param Boolean $useservercerts * @return Array */ private function getCF($user, $apikey, $authurl, $useservercerts = false) { $storage = $this->get_storage(); if (!empty($storage)) return $storage; if (!class_exists('UpdraftPlus_CF_Authentication')) updraft_try_include_file('includes/cloudfiles/cloudfiles.php', 'include_once'); if (!defined('UPDRAFTPLUS_SSL_DISABLEVERIFY')) define('UPDRAFTPLUS_SSL_DISABLEVERIFY', UpdraftPlus_Options::get_updraft_option('updraft_ssl_disableverify')); $auth = new UpdraftPlus_CF_Authentication($user, trim($apikey), null, $authurl); $this->log("authentication URL: $authurl"); $auth->authenticate(); $storage = new UpdraftPlus_CF_Connection($auth); if (!$useservercerts) $storage->ssl_use_cabundle(UPDRAFTPLUS_DIR.'/includes/cacert.pem'); $this->set_storage($storage); return $storage; } /** * This method overrides the parent method and lists the supported features of this remote storage option. * * @return Array - an array of supported features (any features not * mentioned are assumed to not be supported) */ public function get_supported_features() { // This options format is handled via only accessing options via $this->get_options() return array('multi_options', 'config_templates', 'multi_storage', 'conditional_logic'); } /** * Retrieve default options for this remote storage module. * * @return Array - an array of options */ public function get_default_options() { return array( 'user' => '', 'authurl' => 'https://auth.api.rackspacecloud.com', 'apikey' => '', 'path' => '', 'region' => null ); } /** * Check whether options have been set up by the user, or not * * @param Array $opts - the potential options * * @return Boolean */ public function options_exist($opts) { if (is_array($opts) && isset($opts['user']) && '' != $opts['user'] && !empty($opts['apikey'])) return true; return false; } public function backup($backup_array) { global $updraftplus; $opts = $this->get_options(); $updraft_dir = $updraftplus->backups_dir_location().'/'; $container = $opts['path']; try { $storage = $this->getCF($opts['user'], $opts['apikey'], $opts['authurl'], UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts')); $container_object = $storage->create_container($container); } catch (AuthenticationException $e) { $this->log('authentication failed ('.$e->getMessage().')'); $this->log(__('authentication failed', 'updraftplus').' ('.$e->getMessage().')', 'error'); return false; } catch (NoSuchAccountException $s) { $this->log('authentication failed ('.$e->getMessage().')'); $this->log(__('authentication failed', 'updraftplus').' ('.$e->getMessage().')', 'error'); return false; } catch (Exception $e) { $this->log('error - failed to create and access the container ('.$e->getMessage().')'); $this->log(__('error - failed to create and access the container', 'updraftplus').' ('.$e->getMessage().')', 'error'); return false; } $chunk_size = 5*1024*1024; foreach ($backup_array as $file) { $fullpath = $updraft_dir.$file; $orig_file_size = filesize($fullpath); // $cfpath = ($path == '') ? $file : "$path/$file"; // $chunk_path = ($path == '') ? "chunk-do-not-delete-$file" : "$path/chunk-do-not-delete-$file"; $cfpath = $file; $chunk_path = "chunk-do-not-delete-$file"; try { $object = new UpdraftPlus_CF_Object($container_object, $cfpath); $object->content_type = "application/zip"; $uploaded_size = (isset($object->content_length)) ? $object->content_length : 0; if ($uploaded_size <= $orig_file_size) { $fp = @fopen($fullpath, "rb");// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise because of the function. if (!$fp) { $this->log("failed to open file: $fullpath"); $this->log("$file: ".__('Error: Failed to open local file', 'updraftplus'), 'error'); return false; } $chunks = floor($orig_file_size / $chunk_size); // There will be a remnant unless the file size was exactly on a 5MB boundary if ($orig_file_size % $chunk_size > 0) $chunks++; $this->log("upload: $file (chunks: $chunks) -> cloudfiles://$container/$cfpath ($uploaded_size)"); if ($chunks < 2) { try { $object->load_from_filename($fullpath); $this->log("regular upload: success"); $updraftplus->uploaded_file($file); } catch (Exception $e) { $this->log("regular upload: failed ($file) (".$e->getMessage().")"); $this->log("$file: ".__('Error: Failed to upload', 'updraftplus'), 'error'); } } else { $errors_so_far = 0; for ($i = 1; $i <= $chunks; $i++) { $upload_start = ($i-1)*$chunk_size; // The file size -1 equals the byte offset of the final byte $upload_end = min($i*$chunk_size-1, $orig_file_size-1); $upload_remotepath = $chunk_path."_$i"; // Don't forget the +1; otherwise the last byte is omitted $upload_size = $upload_end - $upload_start + 1; $chunk_object = new UpdraftPlus_CF_Object($container_object, $upload_remotepath); $chunk_object->content_type = "application/zip"; // Without this, some versions of Curl add Expect: 100-continue, which results in Curl then giving this back: curl error: 55) select/poll returned error // Didn't make the difference - instead we just check below for actual success even when Curl reports an error // $chunk_object->headers = array('Expect' => ''); $remote_size = (isset($chunk_object->content_length)) ? $chunk_object->content_length : 0; if ($remote_size >= $upload_size) { $this->log("Chunk $i ($upload_start - $upload_end): already uploaded"); } else { $this->log("Chunk $i ($upload_start - $upload_end): begin upload"); // Upload the chunk fseek($fp, $upload_start); try { $chunk_object->write($fp, $upload_size, false); $updraftplus->record_uploaded_chunk(round(100*$i/$chunks, 1), $i, $fullpath); } catch (Exception $e) { $this->log("chunk upload: error: ($file / $i) (".$e->getMessage().")"); // Experience shows that Curl sometimes returns a select/poll error (curl error 55) even when everything succeeded. Google seems to indicate that this is a known bug. $chunk_object = new UpdraftPlus_CF_Object($container_object, $upload_remotepath); $chunk_object->content_type = "application/zip"; $remote_size = (isset($chunk_object->content_length)) ? $chunk_object->content_length : 0; if ($remote_size >= $upload_size) { $this->log("$file: Chunk now exists; ignoring error (presuming it was an apparently known curl bug)"); } else { $this->log("$file: ".__('Error: Failed to upload', 'updraftplus'), 'error'); $errors_so_far++; if ($errors_so_far >=3) return false; } } } } if ($errors_so_far) return false; // All chunks are uploaded - now upload the manifest try { $object->manifest = $container."/".$chunk_path."_"; // Put a zero-length file $object->write("", 0, false); $object->sync_manifest(); $this->log("upload: success"); $updraftplus->uploaded_file($file); // } catch (InvalidResponseException $e) { } catch (Exception $e) { $this->log('error - failed to re-assemble chunks ('.$e->getMessage().')'); $this->log(__('error - failed to re-assemble chunks', 'updraftplus').' ('.$e->getMessage().')', 'error'); return false; } } } } catch (Exception $e) { $this->log(__('error - failed to upload file', 'updraftplus').' ('.$e->getMessage().')'); $this->log(__('error - failed to upload file', 'updraftplus').' ('.$e->getMessage().')', 'error'); return false; } } return array('cloudfiles_object' => $container_object, 'cloudfiles_orig_path' => $opts['path'], 'cloudfiles_container' => $container); } public function listfiles($match = 'backup_') { $opts = $this->get_options(); $container = $opts['path']; if (empty($opts['user']) || empty($opts['apikey'])) new WP_Error('no_settings', __('No settings were found', 'updraftplus')); try { $storage = $this->getCF($opts['user'], $opts['apikey'], $opts['authurl'], UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts')); $container_object = $storage->create_container($container); } catch (Exception $e) { return new WP_Error('no_access', sprintf(__('%s authentication failed', 'updraftplus'), 'Cloud Files').' ('.$e->getMessage().')'); } $results = array(); try { $objects = $container_object->list_objects(0, null, $match); foreach ($objects as $name) { $result = array('name' => $name); try { $object = new UpdraftPlus_CF_Object($container_object, $name, true); if (0 == $object->content_length) { $result = false; } else { $result['size'] = $object->content_length; } } catch (Exception $e) { // Catch } if (is_array($result)) $results[] = $result; } } catch (Exception $e) { return new WP_Error('cf_error', 'Cloud Files error ('.$e->getMessage().')'); } return $results; } /** * Delete a single file from the service using the CloudFiles API * * @param Array $files - array of file paths to delete * @param Array $cloudfilesarr - CloudFiles container and object details * @param Array $sizeinfo - unused here * @return Boolean|String - either a boolean true or an error code string */ public function delete($files, $cloudfilesarr = false, $sizeinfo = array()) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $sizeinfo is unused if (is_string($files)) $files =array($files); if ($cloudfilesarr) { $container_object = $cloudfilesarr['cloudfiles_object']; $container = $cloudfilesarr['cloudfiles_container']; } else { try { $opts = $this->get_options(); $container = $opts['path']; $storage = $this->getCF($opts['user'], $opts['apikey'], $opts['authurl'], UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts')); $container_object = $storage->create_container($container); } catch (Exception $e) { $this->log('authentication failed ('.$e->getMessage().')'); $this->log(__('authentication failed', 'updraftplus').' ('.$e->getMessage().')', 'error'); return 'authentication_fail'; } } $ret = true; foreach ($files as $file) { $fpath = $file; $this->log("Delete remote: container=$container, path=$fpath"); // We need to search for chunks $chunk_path = "chunk-do-not-delete-$file"; try { $objects = $container_object->list_objects(0, null, $chunk_path.'_'); foreach ($objects as $chunk) { $this->log('Chunk to delete: '.$chunk); $container_object->delete_object($chunk); $this->log('Chunk deleted: '.$chunk); } } catch (Exception $e) { $this->log('chunk delete failed: '.$e->getMessage()); } try { $container_object->delete_object($fpath); $this->log('Deleted: '.$fpath); } catch (Exception $e) { $this->log('delete failed: '.$e->getMessage()); $ret = 'file_delete_error'; } } return $ret; } public function download($file) { global $updraftplus; $updraft_dir = $updraftplus->backups_dir_location(); $opts = $this->get_options(); try { $storage = $this->getCF($opts['user'], $opts['apikey'], $opts['authurl'], UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts')); } catch (AuthenticationException $e) { $this->log('authentication failed ('.$e->getMessage().')'); $this->log(__('authentication failed', 'updraftplus').' ('.$e->getMessage().')', 'error'); return false; } catch (NoSuchAccountException $s) { $this->log('authentication failed ('.$e->getMessage().')'); $this->log(__('authentication failed', 'updraftplus').' ('.$e->getMessage().')', 'error'); return false; } catch (Exception $e) { $this->log('error - failed to create and access the container ('.$e->getMessage().')'); $this->log(__('error - failed to create and access the container', 'updraftplus').' ('.$e->getMessage().')', 'error'); return false; } $path = untrailingslashit($opts['path']); $container = $path; try { $container_object = $storage->create_container($container); } catch (Exception $e) { $this->log('error - failed to create and access the container ('.$e->getMessage().')'); $this->log(__('error - failed to create and access the container', 'updraftplus').' ('.$e->getMessage().')', 'error'); return false; } $path = $file; $this->log("download: cloudfiles://$container/$path"); try { // The third parameter causes an exception to be thrown if the object does not exist remotely $object = new UpdraftPlus_CF_Object($container_object, $path, true); $fullpath = $updraft_dir.'/'.$file; $start_offset = (file_exists($fullpath)) ? filesize($fullpath) : 0; // Get file size from remote - see if we've already finished $remote_size = $object->content_length; if ($start_offset >= $remote_size) { $this->log("file is already completely downloaded ($start_offset/$remote_size)"); return true; } // Some more remains to download - so let's do it if (!$fh = fopen($fullpath, 'a')) { $this->log("Error opening local file: $fullpath"); $this->log("$file: ".__('Error opening local file: Failed to download', 'updraftplus'), 'error'); return false; } $headers = array(); // If resuming, then move to the end of the file if ($start_offset) { $this->log("local file is already partially downloaded ($start_offset/$remote_size)"); fseek($fh, $start_offset); $headers['Range'] = "bytes=$start_offset-"; } // Now send the request itself try { $object->stream($fh, $headers); } catch (Exception $e) { $this->log("Failed to download: $file (".$e->getMessage().")"); $this->log("$file: ".__('Error downloading remote file: Failed to download', 'updraftplus').' ('.$e->getMessage().")", 'error'); return false; } // All-in-one-go method: // $object->save_to_filename($fullpath); } catch (NoSuchObjectException $e) { $this->log('error - no such file exists. ('.$e->getMessage().')'); $this->log(__('Error - no such file exists.', 'updraftplus').' ('.$e->getMessage().')', 'error'); return false; } catch (Exception $e) { $this->log('error - failed to download the file ('.$e->getMessage().')'); $this->log(__('Error - failed to download the file', 'updraftplus').' ('.$e->getMessage().')', 'error'); return false; } return true; } /** * Get the pre configuration template * * @return String - the template */ public function get_pre_configuration_template() { global $updraftplus_admin; $classes = $this->get_css_classes(false); ?> <tr class="<?php echo esc_attr($classes) . ' ' . 'cloudfiles_pre_config_container';?>"> <td colspan="2"> <img alt="Rackspace Cloud Files" src="<?php echo esc_url(UPDRAFTPLUS_URL);?>/images/rackspacecloud-logo.png"><br> <?php // Check requirements. global $updraftplus_admin; if (!function_exists('mb_substr')) { $updraftplus_admin->show_double_warning('<strong>'.__('Warning', 'updraftplus').':</strong> '.sprintf(__('Your web server\'s PHP installation does not included a required module (%s).', 'updraftplus'), 'mbstring').' '.__('Please contact your web hosting provider\'s support.', 'updraftplus').' '.sprintf(__("UpdraftPlus's %s module <strong>requires</strong> %s.", 'updraftplus'), 'Cloud Files', 'mbstring').' '.__('Please do not file any support requests; there is no alternative.', 'updraftplus'), 'cloudfiles', false); } $updraftplus_admin->curl_check('Rackspace Cloud Files', false, 'cloudfiles', false); ?> <p> <?php printf( // translators: %1$s - opening link tag to Rackspace Cloud console, %2$s - closing link tag, %3$s - opening link tag to instructions. esc_html__('Get your API key from your %1$sRackspace Cloud console%2$s (%3$sread instructions here%2$s), then pick a container name to use for storage.', 'updraftplus'), '<a href="https://mycloud.rackspace.com/" target="_blank">', '</a>', '<a href="http://www.rackspace.com/knowledge_center/article/rackspace-cloud-essentials-1-generating-your-api-key" target="_blank">' ); echo ' '.esc_html__('This container will be created for you if it does not already exist.', 'updraftplus').' <a href="https://updraftplus.com/faqs/there-appear-to-be-lots-of-extra-files-in-my-rackspace-cloud-files-container/" target="_blank">'.esc_html__('Also, you should read this important FAQ.', 'updraftplus').'</a>'; ?> </p> </td> </tr> <?php } /** * Get the configuration template * * @return String - the template, ready for substitutions to be carried out */ public function get_configuration_template() { $classes = $this->get_css_classes(); $template_str = ' <tr class="'.$classes.'"> <th>'.__('US or UK Cloud', 'updraftplus').':</th> <td> <select data-updraft_settings_test="authurl" '.$this->output_settings_field_name_and_id('authurl', true).'> <option {{#ifeq "https://auth.api.rackspacecloud.com" authurl}}selected="selected"{{/ifeq}} value="https://auth.api.rackspacecloud.com">'.__('US (default)', 'updraftplus').'</option> <option {{#ifeq "https://lon.auth.api.rackspacecloud.com" authurl}}selected="selected"{{/ifeq}} value="https://lon.auth.api.rackspacecloud.com">'.__('UK', 'updraftplus').'</option> </select> </td> </tr> <input type="hidden" data-updraft_settings_test="region" '.$this->output_settings_field_name_and_id('region', true).' value="">'; /* // Can put a message here if someone asks why region storage is not available (only available on new SDK) <tr class="updraftplusmethod cloudfiles"> <th><?php _e('Rackspace Storage Region','updraftplus');?>:</th> <td> </td> </tr> */ $template_str .= ' <tr class="'.$classes.'"> <th>'.__('Cloud Files username', 'updraftplus').':</th> <td><input data-updraft_settings_test="user" type="text" autocomplete="off" class="updraft_input--wide" '.$this->output_settings_field_name_and_id('user', true).' value="{{user}}" /></td> </tr> <tr class="'.$classes.'"> <th>'.__('Cloud Files API Key', 'updraftplus').':</th> <td><input data-updraft_settings_test="apikey" type="'.apply_filters('updraftplus_admin_secret_field_type', 'password').'" autocomplete="off" class="updraft_input--wide" '.$this->output_settings_field_name_and_id('apikey', true).' value="{{apikey}}" /> </td> </tr> <tr class="'.$classes.'"> <th>'.apply_filters('updraftplus_cloudfiles_location_description', __('Cloud Files Container', 'updraftplus')).':</th> <td><input data-updraft_settings_test="path" type="text" class="updraft_input--wide" '.$this->output_settings_field_name_and_id('path', true).' value="{{path}}" /></td> </tr>'; $template_str .= $this->get_test_button_html(__('Cloud Files', 'updraftplus')); return $template_str; } /** * Modifies handerbar template options * * @param array $opts handerbar template options * @return Array - Modified handerbar template options */ public function transform_options_for_template($opts) { $opts['apikey'] = trim($opts['apikey']); $opts['authurl'] = isset($opts['authurl']) ? $opts['authurl'] : ''; return $opts; } /** * Perform a test of user-supplied credentials, and echo the result * * @param Array $posted_settings - settings to test */ public function credentials_test($posted_settings) { if (empty($posted_settings['apikey'])) { echo esc_html(sprintf(__("Failure: No %s was given.", 'updraftplus'), __('API key', 'updraftplus'))); return; } if (empty($posted_settings['user'])) { echo esc_html(sprintf(__("Failure: No %s was given.", 'updraftplus'), __('Username', 'updraftplus'))); return; } $key = $posted_settings['apikey']; $user = $posted_settings['user']; $path = $posted_settings['path']; $authurl = $posted_settings['authurl']; $useservercerts = $posted_settings['useservercerts']; $disableverify = $posted_settings['disableverify']; if (preg_match("#^([^/]+)/(.*)$#", $path, $bmatches)) { $container = $bmatches[1]; $path = $bmatches[2]; } else { $container = $path; $path = ""; } if (empty($container)) { esc_html_e("Failure: No container details were given.", 'updraftplus'); return; } define('UPDRAFTPLUS_SSL_DISABLEVERIFY', $disableverify); try { $storage = $this->getCF($user, $key, $authurl, $useservercerts); $container_object = $storage->create_container($container); } catch (AuthenticationException $e) { echo esc_html(__('Cloud Files authentication failed', 'updraftplus').' ('.$e->getMessage().')'); return; } catch (NoSuchAccountException $s) { echo esc_html(__('Cloud Files authentication failed', 'updraftplus').' ('.$e->getMessage().')'); return; } catch (Exception $e) { echo esc_html(__('Cloud Files authentication failed', 'updraftplus').' ('.$e->getMessage().')'); return; } $try_file = md5(rand()).'.txt'; try { $object = $container_object->create_object($try_file); $object->content_type = "text/plain"; $object->write('UpdraftPlus test file'); } catch (Exception $e) { echo esc_html(__('Cloud Files error - we accessed the container, but failed to create a file within it', 'updraftplus').' ('.$e->getMessage().')'); return; } echo esc_html(__('Success', 'updraftplus').": ".__('We accessed the container, and were able to create files within it.', 'updraftplus')); @$container_object->delete_object($try_file);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise because of the method. } } // Moved to the bottom to fix a bug in some version or install of PHP which required UpdraftPlus_BackupModule_cloudfiles_oldsdk to be defined earlier in the file (despite the conditionality) - see HS#19911 if (version_compare(PHP_VERSION, '5.3.3', '>=') && (!defined('UPDRAFTPLUS_CLOUDFILES_USEOLDSDK') || UPDRAFTPLUS_CLOUDFILES_USEOLDSDK != true)) { updraft_try_include_file('methods/cloudfiles-new.php', 'include_once'); class UpdraftPlus_BackupModule_cloudfiles extends UpdraftPlus_BackupModule_cloudfiles_opencloudsdk { } } else { class UpdraftPlus_BackupModule_cloudfiles extends UpdraftPlus_BackupModule_cloudfiles_oldsdk { } } template.php 0000644 00000012253 15231511727 0007077 0 ustar 00 <?php /** * This is a bare-bones to get you started with developing an access method. The methods provided below are all ones you will want to use (though note that the provided email.php method is an * example of truly bare-bones for a method that cannot delete or download and has no configuration). * * Read the existing methods for help. There is no hard-and-fast need to put all your code in this file; it is just for increasing convenience and maintainability; there are no bonus points for 100% elegance. If you need access to some part of WordPress that you can only reach through the main plugin file (updraftplus.php), then go right ahead and patch that. * * Some handy tips: * - Search-and-replace "template" for the name of your access method * - You can also add the methods config_print_javascript_onready and credentials_test if you like * - Name your file accordingly (it is now template.php) * - Add the method to the array $backup_methods in updraftplus.php when ready * - Use the constant UPDRAFTPLUS_DIR to reach Updraft's plugin directory * - Call $updraftplus->log("my log message") to log things, which greatly helps debugging * - UpdraftPlus is licenced under the GPLv3 or later. In order to combine your backup method with UpdraftPlus, you will need to licence to anyone and everyone that you distribute it to in a compatible way. */ if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed.'); if (!class_exists('UpdraftPlus_BackupModule')) updraft_try_include_file('methods/backup-module.php', 'require_once'); class UpdraftPlus_BackupModule_template extends UpdraftPlus_BackupModule { /** * backup method: takes an array, and shovels them off to the cloud storage * * @param Array $backup_array Array of files (basenames) to sent to remote storage * @return Mixed - (boolean)false to indicate failure; otherwise, something to be passed back when deleting files */ public function backup($backup_array) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- This is a template file and can be ignored global $updraftplus;// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- This is a template file and can be ignored // foreach ($backup_array as $file) { // Do our uploading stuff... // If successful, then you must do this: // $updraftplus->uploaded_file($file); // } } /** * This function lists the files found in the configured storage location * * @param String $match a substring to require (tested via strpos() !== false) * * @return Array - each file is represented by an array with entries 'name' and (optional) 'size' */ public function listfiles($match = 'backup_') {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- This is a template file and can be ignored // This function needs to return an array of arrays. The keys for the sub-arrays are name (a path-less filename, i.e. a basename), (optional)size, and should be a list of matching files from the storage backend. A WP_Error object can also be returned; and the error code should be no_settings if that is relevant. return array(); } /** * delete method: takes an array of file names (base name) or a single string, and removes them from the cloud storage * * @param string $files The specific files * @param mixed $data Anything passed back from self::backup() * @param array $sizeinfo Size information * @return Boolean - whether the operation succeeded or not */ public function delete($files, $data = false, $sizeinfo = array()) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- This is a template file and can be ignored global $updraftplus;// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- This is a template file and can be ignored if (is_string($files)) $files = array($files); } /** * download method: takes a file name (base name), and brings it back from the cloud storage into Updraft's directory * You can register errors with $updraftplus->log("my error message", 'error') * * @param String $file The specific file to be downloaded from the Cloud Storage * * @return Boolean - success or failure state */ public function download($file) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- This is a template file and can be ignored global $updraftplus;// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- This is a template file and can be ignored } public function get_supported_features() { // This options format is handled via only accessing options via $this->get_options() return array('multi_options', 'config_templates'); } /** * Get the configuration template, in Handlebars format. * Note that logging is not available from this context; it will do nothing. * * @return String - the template, ready for substitutions to be carried out */ public function get_configuration_template() { ob_start(); $classes = $this->get_css_classes(); ?> <tr class="updraftplusmethod <?php echo esc_attr($classes);?>"> <th>My Method:</th> <td> </td> </tr> <?php return ob_get_clean(); } } ftp.php 0000644 00000045476 15231511727 0006072 0 ustar 00 <?php if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed.'); // Converted to array options: yes // Converted to job_options: yes // Migrate options to new-style storage - May 2014 if (!is_array(UpdraftPlus_Options::get_updraft_option('updraft_ftp')) && '' != UpdraftPlus_Options::get_updraft_option('updraft_server_address', '')) { $opts = array( 'user' => UpdraftPlus_Options::get_updraft_option('updraft_ftp_login'), 'pass' => UpdraftPlus_Options::get_updraft_option('updraft_ftp_pass'), 'host' => UpdraftPlus_Options::get_updraft_option('updraft_server_address'), 'path' => UpdraftPlus_Options::get_updraft_option('updraft_ftp_remote_path'), 'passive' => true ); UpdraftPlus_Options::update_updraft_option('updraft_ftp', $opts); UpdraftPlus_Options::delete_updraft_option('updraft_server_address'); UpdraftPlus_Options::delete_updraft_option('updraft_ftp_pass'); UpdraftPlus_Options::delete_updraft_option('updraft_ftp_remote_path'); UpdraftPlus_Options::delete_updraft_option('updraft_ftp_login'); } if (!class_exists('UpdraftPlus_BackupModule')) updraft_try_include_file('methods/backup-module.php', 'require_once'); class UpdraftPlus_BackupModule_ftp extends UpdraftPlus_BackupModule { /** * Get FTP object with parameters set * * @param String $server Specify Server * @param String $user Specify Username * @param String $pass Specify Password * @param Boolean $disable_ssl Indicate whether to disable SSL * @param Boolean $disable_verify Indicate whether to disable verifiction * @param Boolean $use_server_certs Indicate whether to use server certificates * @param Boolean $passive Indicate whether to use passive FTP mode * @return Array */ private function getFTP($server, $user, $pass, $disable_ssl = false, $disable_verify = true, $use_server_certs = false, $passive = true) { if ('' == trim($server) || '' == trim($user) || '' == trim($pass)) return new WP_Error('no_settings', sprintf(__('No %s settings were found', 'updraftplus'), 'FTP')); if (!class_exists('UpdraftPlus_ftp_wrapper')) updraft_try_include_file('includes/ftp.class.php', 'include_once'); $port = 21; if (preg_match('/^(.*):(\d+)$/', $server, $matches)) { $server = $matches[1]; $port = $matches[2]; } $ftp = new UpdraftPlus_ftp_wrapper($server, $user, $pass, $port); if ($disable_ssl) $ftp->ssl = false; $ftp->use_server_certs = $use_server_certs; $ftp->disable_verify = $disable_verify; $ftp->passive = ($passive) ? true : false; return $ftp; } /** * WordPress options filter, sanitising the FTP options saved from the options page * * @param Array $settings - the options, prior to sanitisation * * @return Array - the sanitised options for saving */ public function options_filter($settings) { if (is_array($settings) && !empty($settings['version']) && !empty($settings['settings'])) { foreach ($settings['settings'] as $instance_id => $instance_settings) { if (!empty($instance_settings['host']) && preg_match('#ftp(es|s)?://(.*)#i', $instance_settings['host'], $matches)) { $settings['settings'][$instance_id]['host'] = rtrim($matches[2], "/ \t\n\r\0x0B"); } if (isset($instance_settings['pass'])) { $settings['settings'][$instance_id]['pass'] = trim($instance_settings['pass'], "\n\r\0\x0B"); } } } return $settings; } public function get_supported_features() { // The 'multi_options' options format is handled via only accessing options via $this->get_options() return array('multi_options', 'config_templates', 'multi_storage', 'conditional_logic'); } public function get_default_options() { return array( 'host' => '', 'user' => '', 'pass' => '', 'path' => '', 'passive' => 1 ); } /** * Retrieve a list of template properties by taking all the persistent variables and methods of the parent class and combining them with the ones that are unique to this module, also the necessary HTML element attributes and texts which are also unique only to this backup module * NOTE: Please sanitise all strings that are required to be shown as HTML content on the frontend side (i.e. wp_kses()) * * @return Array an associative array keyed by names that describe themselves as they are */ public function get_template_properties() { global $updraftplus; $possible = $this->ftp_possible(); $ftp_not_possible = array(); if (is_array($possible)) { $trans = array( 'ftp' => __('regular non-encrypted FTP', 'updraftplus'), 'ftpsslimplicit' => __('encrypted FTP (implicit encryption)', 'updraftplus'), 'ftpsslexplicit' => __('encrypted FTP (explicit encryption)', 'updraftplus') ); foreach ($possible as $type => $missing) { $ftp_not_possible[] = wp_kses('<strong>'.__('Warning', 'updraftplus').':</strong> '. sprintf(__("Your web server's PHP installation has these functions disabled: %s.", 'updraftplus'), implode(', ', $missing)).' '.sprintf(__('Your hosting company must enable these functions before %s can work.', 'updraftplus'), $trans[$type]), $this->allowed_html_for_content_sanitisation()); } } $properties = array( 'updraft_sftp_ftps_notice' => wp_kses(apply_filters('updraft_sftp_ftps_notice', '<strong>'.__('Only non-encrypted FTP is supported by regular UpdraftPlus.').'</strong> <a href="'.esc_url($updraftplus->get_url('premium')).'" target="_blank">'.__('If you want encryption (e.g. you are storing sensitive business data), then an add-on is available in the Premium version.', 'updraftplus')), $this->allowed_html_for_content_sanitisation()), 'ftp_not_possible_warnings' => $ftp_not_possible, 'input_host_label' => __('FTP server', 'updraftplus'), 'input_user_label' => __('FTP login', 'updraftplus'), 'input_password_label' => __('FTP password', 'updraftplus'), 'input_password_type' => apply_filters('updraftplus_admin_secret_field_type', 'password'), 'input_path_label' => __('Remote path', 'updraftplus'), 'input_path_title' => __('Needs to already exist', 'updraftplus'), 'input_passive_label' => __('Passive mode', 'updraftplus'), 'input_passive_title' => __('Almost all FTP servers will want passive mode; but if you need active mode, then uncheck this.', 'updraftplus'), 'input_test_label' => sprintf(__('Test %s Settings', 'updraftplus'), $updraftplus->backup_methods[$this->get_id()]) ); return wp_parse_args($properties, $this->get_persistent_variables_and_methods()); } public function backup($backup_array) { global $updraftplus; $opts = $this->get_options(); $ftp = $this->getFTP( $opts['host'], $opts['user'], $opts['pass'], $updraftplus->get_job_option('updraft_ssl_nossl'), $updraftplus->get_job_option('updraft_ssl_disableverify'), $updraftplus->get_job_option('updraft_ssl_useservercerts'), $opts['passive'] ); if (is_wp_error($ftp) || !$ftp->connect()) { if (is_wp_error($ftp)) { $updraftplus->log_wp_error($ftp); } else { $this->log("Failure: we did not successfully log in with those credentials."); } $this->log(__("login failure", 'updraftplus'), 'error'); return false; } // $ftp->make_dir(); we may need to recursively create dirs? TODO $updraft_dir = $updraftplus->backups_dir_location().'/'; $ftp_remote_path = trailingslashit($opts['path']); foreach ($backup_array as $file) { $fullpath = $updraft_dir.$file; $this->log("upload attempt: $file -> ftp://".$opts['user']."@".$opts['host']."/{$ftp_remote_path}{$file}"); $timer_start = microtime(true); $size_k = round(filesize($fullpath)/1024, 1); // Note :Setting $resume to true unnecessarily is not meant to be a problem. Only ever (Feb 2014) seen one weird FTP server where calling SIZE on a non-existent file did create a problem. So, this code just helps that case. (the check for non-empty upload_status[p] is being cautious. $upload_status = $updraftplus->jobdata_get('uploading_substatus'); if (0 == $updraftplus->current_resumption || (is_array($upload_status) && !empty($upload_status['p']) && 0 == $upload_status['p'])) { $resume = false; } else { $resume = true; } if ($ftp->put($fullpath, $ftp_remote_path.$file, FTP_BINARY, $resume, $updraftplus)) { $this->log("upload attempt successful (".$size_k."KB in ".(round(microtime(true)-$timer_start, 2)).'s)'); $updraftplus->uploaded_file($file); } else { $this->log("ERROR: FTP upload failed"); $this->log(__("upload failed", 'updraftplus'), 'error'); } } return array('ftp_object' => $ftp, 'ftp_remote_path' => $ftp_remote_path); } public function listfiles($match = 'backup_') { global $updraftplus; $opts = $this->get_options(); $ftp = $this->getFTP( $opts['host'], $opts['user'], $opts['pass'], $updraftplus->get_job_option('updraft_ssl_nossl'), $updraftplus->get_job_option('updraft_ssl_disableverify'), $updraftplus->get_job_option('updraft_ssl_useservercerts'), $opts['passive'] ); if (is_wp_error($ftp)) return $ftp; if (!$ftp->connect()) return new WP_Error('ftp_login_failed', sprintf(__("%s login failure", 'updraftplus'), 'FTP')); $ftp_remote_path = $opts['path']; if ($ftp_remote_path) $ftp_remote_path = trailingslashit($ftp_remote_path); $dirlist = $ftp->dir_list($ftp_remote_path); if (!is_array($dirlist)) return array(); $results = array(); foreach ($dirlist as $k => $path) { if ($ftp_remote_path) { // Feb 2015 - found a case where the directory path was not prefixed on if (0 !== strpos($path, $ftp_remote_path) && (false !== strpos('/', $ftp_remote_path) && false !== strpos('\\', $ftp_remote_path))) continue; if (0 === strpos($path, $ftp_remote_path)) $path = substr($path, strlen($ftp_remote_path)); // if (0 !== strpos($path, $ftp_remote_path)) continue; // $path = substr($path, strlen($ftp_remote_path)); if (0 === strpos($path, $match)) $results[]['name'] = $path; } else { if ('/' == substr($path, 0, 1)) $path = substr($path, 1); if (false !== strpos($path, '/')) continue; if (0 === strpos($path, $match)) $results[]['name'] = $path; } unset($dirlist[$k]); } // ftp_nlist() doesn't return file sizes. rawlist() does, but is tricky to parse. So, we get the sizes manually. foreach ($results as $ind => $name) { $size = $ftp->size($ftp_remote_path.$name['name']); if (0 === $size) { unset($results[$ind]); } elseif ($size>0) { $results[$ind]['size'] = $size; } } return $results; } /** * Delete a single file from the service using FTP protocols * * @param Array $files - array of file names to delete * @param Array $ftparr - FTP details/credentials * @param Array $sizeinfo - unused here * @return Boolean|String - either a boolean true or an error code string */ public function delete($files, $ftparr = array(), $sizeinfo = array()) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $sizeinfo unused global $updraftplus; if (is_string($files)) $files = array($files); $opts = $this->get_options(); if (is_array($ftparr) && isset($ftparr['ftp_object'])) { $ftp = $ftparr['ftp_object']; } else { $ftp = $this->getFTP( $opts['host'], $opts['user'], $opts['pass'], $updraftplus->get_job_option('updraft_ssl_nossl'), $updraftplus->get_job_option('updraft_ssl_disableverify'), $updraftplus->get_job_option('updraft_ssl_useservercerts'), $opts['passive'] ); if (is_wp_error($ftp) || !$ftp->connect()) { if (is_wp_error($ftp)) $updraftplus->log_wp_error($ftp); $this->log("Failure: we did not successfully log in with those credentials (host=".$opts['host'].")."); return 'authentication_fail'; } } $ftp_remote_path = isset($ftparr['ftp_remote_path']) ? $ftparr['ftp_remote_path'] : trailingslashit($opts['path']); $ret = true; foreach ($files as $file) { if (@$ftp->delete($ftp_remote_path.$file)) {// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise because of the method. $this->log("delete: succeeded ({$ftp_remote_path}{$file})"); } else { $this->log("delete: failed ({$ftp_remote_path}{$file})"); $ret = 'file_delete_error'; } } return $ret; } public function download($file) { global $updraftplus; $opts = $this->get_options(); $ftp = $this->getFTP( $opts['host'], $opts['user'], $opts['pass'], $updraftplus->get_job_option('updraft_ssl_nossl'), $updraftplus->get_job_option('updraft_ssl_disableverify'), $updraftplus->get_job_option('updraft_ssl_useservercerts'), $opts['passive'] ); if (is_wp_error($ftp)) { $this->log('Failure to get FTP object: '.$ftp->get_error_code().': '.$ftp->get_error_message()); $this->log($ftp->get_error_message().' ('.$ftp->get_error_code().')', 'error'); return false; } if (!$ftp->connect()) { $this->log('Failure: we did not successfully log in with those credentials.'); $this->log(__('login failure', 'updraftplus'), 'error'); return false; } // $ftp->make_dir(); we may need to recursively create dirs? TODO $ftp_remote_path = trailingslashit($opts['path']); $fullpath = $updraftplus->backups_dir_location().'/'.$file; $resume = false; if (file_exists($fullpath)) { $resume = true; $this->log("File already exists locally; will resume: size: ".filesize($fullpath)); } return $ftp->get($fullpath, $ftp_remote_path.$file, FTP_BINARY, $resume, $updraftplus); } private function ftp_possible() { $funcs_disabled = array(); foreach (array('ftp_connect', 'ftp_login', 'ftp_nb_fput') as $func) { if (!function_exists($func)) $funcs_disabled['ftp'][] = $func; } $funcs_disabled = apply_filters('updraftplus_ftp_possible', $funcs_disabled); return (0 == count($funcs_disabled)) ? true : $funcs_disabled; } /** * Get the pre configuration template * * @return String - the template */ public function get_pre_configuration_template() { ?> <tr class="{{get_template_css_classes false}} ftp_pre_config_container"> <td colspan="2"> <h3>{{method_display_name}}</h3> {{#each ftp_not_possible_warnings}} <div class="error updraftplusmethod ftp"><p>{{{this}}}</p></div> <div class="notice error below-h2"><p>{{{this}}}</p></div> {{/each}} {{#ifCond "undefined" "not_typeof" updraft_sftp_ftps_notice}} <em><p>{{{updraft_sftp_ftps_notice}}}</p></em> {{/ifCond}} </td> </tr> <?php } /** * Get the configuration template * * @return String - the template, ready for substitutions to be carried out */ public function get_configuration_template() { ob_start(); ?> <tr class="{{get_template_css_classes true}}"> <th>{{input_host_label}}:</th> <td><input class="updraft_input--wide" type="text" size="40" data-updraft_settings_test="server" id="{{get_template_input_attribute_value "id" "host"}}" name="{{get_template_input_attribute_value "name" "host"}}" value="{{host}}" /></td> </tr> <tr class="{{get_template_css_classes true}}"> <th>{{input_user_label}}:</th> <td><input class="updraft_input--wide" type="text" size="40" data-updraft_settings_test="login" id="{{get_template_input_attribute_value "id" "user"}}" name="{{get_template_input_attribute_value "name" "user"}}" value="{{user}}" /></td> </tr> <tr class="{{get_template_css_classes true}}"> <th>{{input_password_label}}:</th> <td><input class="updraft_input--wide" type="{{input_password_type}}" size="40" data-updraft_settings_test="pass" id="{{get_template_input_attribute_value "id" "pass"}}" name="{{get_template_input_attribute_value "name" "pass"}}" value="{{pass}}" /></td> </tr> <tr class="{{get_template_css_classes true}}"> <th>{{input_path_label}}:</th> <td><input title="{{input_path_title}}" class="updraft_input--wide" type="text" size="64" data-updraft_settings_test="path" id="{{get_template_input_attribute_value "id" "path"}}" name="{{get_template_input_attribute_value "name" "path"}}" value="{{path}}" /> <em>{{input_path_title}}</em></td> </tr> <tr class="{{get_template_css_classes true}}"> <th>{{input_passive_label}}:</th> <td> <input title="{{input_passive_title}}" type="checkbox" data-updraft_settings_test="passive" id="{{get_template_input_attribute_value "id" "passive"}}" name="{{get_template_input_attribute_value "name" "passive"}}" value="1" {{#ifeq '1' passive}}checked="checked"{{/ifeq}}> <br><em>{{input_passive_title}}</em></td> </tr> {{{get_template_test_button_html "FTP"}}} <?php return ob_get_clean(); } /** * Perform a test of user-supplied credentials, and echo the result * * @param Array $posted_settings - settings to test */ public function credentials_test($posted_settings) { $server = $posted_settings['server']; $login = $posted_settings['login']; $pass = $posted_settings['pass']; $path = $posted_settings['path']; $nossl = $posted_settings['nossl']; $passive = empty($posted_settings['passive']) ? false : true; $disable_verify = $posted_settings['disableverify']; $use_server_certs = $posted_settings['useservercerts']; if (empty($server)) { esc_html_e('Failure: No server details were given.', 'updraftplus'); return; } if (empty($login)) { echo esc_html(sprintf(__('Failure: No %s was given.', 'updraftplus'), __('login', 'updraftplus'))); return; } if (empty($pass)) { echo esc_html(sprintf(__('Failure: No %s was given.', 'updraftplus'), __('password', 'updraftplus'))); return; } if (preg_match('#ftp(es|s)?://(.*)#i', $server, $matches)) $server = untrailingslashit($matches[2]); // $ftp = $this->getFTP($server, $login, $pass, $nossl, $disable_verify, $use_server_certs); $ftp = $this->getFTP($server, $login, $pass, $nossl, $disable_verify, $use_server_certs, $passive); if (!$ftp->connect()) { esc_html_e('Failure: we did not successfully log in with those credentials.', 'updraftplus'); return; } // $ftp->make_dir(); we may need to recursively create dirs? TODO $file = md5(rand(0, 99999999)).'.tmp'; $fullpath = trailingslashit($path).$file; if ($ftp->put(ABSPATH.WPINC.'/version.php', $fullpath, FTP_BINARY, false, true)) { echo esc_html(__("Success: we successfully logged in, and confirmed our ability to create a file in the given directory (login type:", 'updraftplus')." ".$ftp->login_type.')'); @$ftp->delete($fullpath);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise because of the method. } else { esc_html_e('Failure: we successfully logged in, but were not able to create a file in the given directory.', 'updraftplus'); if (!empty($ftp->ssl)) { echo ' '.esc_html__('This is sometimes caused by a firewall - try turning off SSL in the expert settings, and testing again.', 'updraftplus'); } } } /** * Check whether options have been set up by the user, or not * * @param Array $opts - the potential options * * @return Boolean */ public function options_exist($opts) { if (is_array($opts) && !empty($opts['host']) && isset($opts['user']) && '' != $opts['user']) return true; return false; } } sftp.php 0000644 00000002244 15231511727 0006237 0 ustar 00 <?php if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed.'); if (class_exists('UpdraftPlus_Addons_RemoteStorage_sftp')) { // Migrate options to standard-style - April 2017. This then enables them to get picked up by the multi-options settings translation code if (!is_array(UpdraftPlus_Options::get_updraft_option('updraft_sftp')) && '' != UpdraftPlus_Options::get_updraft_option('updraft_sftp_settings', '')) { $opts = UpdraftPlus_Options::get_updraft_option('updraft_sftp_settings'); UpdraftPlus_Options::update_updraft_option('updraft_sftp', $opts); UpdraftPlus_Options::delete_updraft_option('updraft_sftp_settings'); } class UpdraftPlus_BackupModule_sftp extends UpdraftPlus_Addons_RemoteStorage_sftp { public function __construct() { parent::__construct('sftp', 'SFTP/SCP'); } } } else { updraft_try_include_file('methods/addon-not-yet-present.php', 'include_once'); /** * N.B. UpdraftPlus_BackupModule_AddonNotYetPresent extends UpdraftPlus_BackupModule */ class UpdraftPlus_BackupModule_sftp extends UpdraftPlus_BackupModule_AddonNotYetPresent { public function __construct() { parent::__construct('sftp', 'SFTP/SCP'); } } } email.php 0000644 00000013675 15231511727 0006364 0 ustar 00 <?php if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed.'); // Files can easily get too big for this method if (!class_exists('UpdraftPlus_BackupModule')) updraft_try_include_file('methods/backup-module.php', 'require_once'); class UpdraftPlus_BackupModule_email extends UpdraftPlus_BackupModule { public function backup($backup_array) { global $updraftplus; $updraft_dir = trailingslashit($updraftplus->backups_dir_location()); $email = $updraftplus->just_one_email(UpdraftPlus_Options::get_updraft_option('updraft_email'), true); if (!is_array($email)) $email = array_filter(array($email)); foreach ($backup_array as $type => $file) { $descrip_type = preg_match('/^(.*)\d+$/', $type, $matches) ? $matches[1] : $type; $fullpath = $updraft_dir.$file; if (file_exists($fullpath) && filesize($fullpath) > UPDRAFTPLUS_WARN_EMAIL_SIZE) { $size_in_mb_of_big_file = round(filesize($fullpath)/1048576, 1); $toobig_hash = md5($file); $this->log($file.': '.sprintf(__('This backup archive is %s MB in size - the attempt to send this via email is likely to fail (few email servers allow attachments of this size).', 'updraftplus'), $size_in_mb_of_big_file).' '.__('If so, you should switch to using a different remote storage method.', 'updraftplus'), 'warning', 'toobigforemail_'.$toobig_hash); } $any_attempted = false; $any_sent = false; $any_skip = false; foreach ($email as $ind => $addr) { if (apply_filters('updraftplus_email_backup', true, $addr, $ind, $type)) { foreach (explode(',', $addr) as $sendmail_addr) { if (!preg_match('/^https?:\/\//i', $sendmail_addr)) { $send_short = (strlen($sendmail_addr)>5) ? substr($sendmail_addr, 0, 5).'...' : $sendmail_addr; $this->log("$file: email to: $send_short"); $any_attempted = true; $headers = array(); $subject = __("WordPress Backup", 'updraftplus').': '.get_bloginfo('name').' (UpdraftPlus '.$updraftplus->version.') '.get_date_from_gmt(gmdate('Y-m-d H:i:s', $updraftplus->backup_time), 'Y-m-d H:i'); $from_email = apply_filters('updraftplus_email_from_header', $updraftplus->get_email_from_header()); $from_name = apply_filters('updraftplus_email_from_name_header', $updraftplus->get_email_from_name_header()); $use_wp_from_name_filter = '' === $from_email; // Notice that we don't use the 'wp_mail_from' filter, but only the 'From:' header to set sender name and sender email address, the reason behind it is that some SMTP plugins override the "wp_mail()" function and they do anything they want inside their own "wp_mail()" function, including not to call the php_mailer filter nor the wp_mail_from and wp_mail_from_name filters, but since the function signature remain the same as the WP one, so they may evaluate and do something with the header parameter if (!$use_wp_from_name_filter) { $headers[] = sprintf('From: %s <%s>', $from_name, $from_email); } else { add_filter('wp_mail_from_name', array($updraftplus, 'get_email_from_name_header'), 9); } add_action('wp_mail_failed', array($updraftplus, 'log_email_delivery_failure')); $sent = wp_mail(trim($sendmail_addr), $subject, sprintf(__("Backup is of: %s.", 'updraftplus'), site_url().' ('.$descrip_type.')'), $headers, array($fullpath)); remove_action('wp_mail_failed', array($updraftplus, 'log_email_delivery_failure')); if ($use_wp_from_name_filter) remove_filter('wp_mail_from_name', array($this, 'get_email_from_name_header'), 9); if ($sent) $any_sent = true; } } } else { $log_message = apply_filters('updraftplus_email_backup_skip_log_message', '', $addr, $ind, $descrip_type); if (!empty($log_message)) { $this->log($log_message); } $any_skip = true; } } if ($any_sent) { if (isset($toobig_hash)) { $updraftplus->log_remove_warning('toobigforemail_'.$toobig_hash); // Don't leave it still set for the next archive unset($toobig_hash); } $updraftplus->uploaded_file($file); } elseif ($any_attempted) { $this->log('Mails were not sent successfully'); $this->log(__('The attempt to send the backup via email failed (probably the backup was too large for this method)', 'updraftplus'), 'error'); } elseif ($any_skip) { $this->log('No email addresses were configured to send email to '.$descrip_type); } else { $this->log('No email addresses were configured to send to'); } } return null; } /** * Acts as a WordPress options filter * * @param Array $options - An array of options * * @return Array - the returned array can either be the set of updated settings or a WordPress error array */ public function options_filter($options) { global $updraftplus; return $updraftplus->just_one_email($options); } public function config_print() { global $updraftplus; ?> <tr class="updraftplusmethod email"> <th><?php esc_html_e('Note:', 'updraftplus');?></th> <td><?php $used = apply_filters('updraftplus_email_whichaddresses', sprintf(__("Your site's admin email address (%s) will be used.", 'updraftplus'), get_bloginfo('admin_email').' - <a href="'.esc_attr(admin_url('options-general.php')).'">'.__("configure it here", 'updraftplus').'</a>'). ' <a href="'.$updraftplus->get_url('premium').'" target="_blank">'.__('For more options, use Premium', 'updraftplus').'</a>' ); $allowed_html = array( 'a' => array( 'href' => array(), 'target' => array(), ) ); echo wp_kses($used.' '.sprintf(__('Be aware that mail servers tend to have size limits; typically around %s MB; backups larger than any limits will likely not arrive.', 'updraftplus'), '10-20'), $allowed_html); ?> </td> </tr> <?php } public function delete($files, $data = null, $sizeinfo = array()) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Unused parameter is present because the caller uses 3 arguments. return true; } } openstack-base.php 0000644 00000056560 15231511727 0010174 0 ustar 00 <?php if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed.'); if (!class_exists('UpdraftPlus_BackupModule')) updraft_try_include_file('methods/backup-module.php', 'require_once'); class UpdraftPlus_BackupModule_openstack_base extends UpdraftPlus_BackupModule { protected $chunk_size; protected $client; protected $method; protected $desc; protected $long_desc; protected $img_url; public function __construct($method, $desc, $long_desc = null, $img_url = '') { $this->method = $method; $this->desc = $desc; $this->long_desc = (is_string($long_desc)) ? $long_desc : $desc; $this->img_url = $img_url; } public function backup($backup_array) { global $updraftplus; $default_chunk_size = (defined('UPDRAFTPLUS_UPLOAD_CHUNKSIZE') && UPDRAFTPLUS_UPLOAD_CHUNKSIZE > 0) ? max(UPDRAFTPLUS_UPLOAD_CHUNKSIZE, 1048576) : 5242880; $this->chunk_size = $updraftplus->jobdata_get('openstack_chunk_size', $default_chunk_size); $opts = $this->get_options(); $this->container = $opts['path']; try { $storage = $this->get_openstack_service($opts, UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts'), UpdraftPlus_Options::get_updraft_option('updraft_ssl_disableverify')); } catch (AuthenticationError $e) { $updraftplus->log($this->desc.' authentication failed ('.$e->getMessage().')'); $updraftplus->log(sprintf(__('%s authentication failed', 'updraftplus'), $this->desc).' ('.$e->getMessage().')', 'error'); return false; } catch (Exception $e) { $updraftplus->log($this->desc.' error - failed to access the container ('.$e->getMessage().') (line: '.$e->getLine().', file: '.$e->getFile().')'); $updraftplus->log(sprintf(__('%s error - failed to access the container', 'updraftplus'), $this->desc).' ('.$e->getMessage().')', 'error'); return false; } // Get the container try { $this->container_object = $storage->getContainer($this->container); } catch (Exception $e) { $updraftplus->log('Could not access '.$this->desc.' container ('.get_class($e).', '.$e->getMessage().') (line: '.$e->getLine().', file: '.$e->getFile().')'); $updraftplus->log(sprintf(__('Could not access %s container', 'updraftplus'), $this->desc).' ('.get_class($e).', '.$e->getMessage().')', 'error'); return false; } foreach ($backup_array as $file) { $file_key = 'status_'.md5($file); $file_status = $this->jobdata_get($file_key, null, 'openstack_'.$file_key); if (is_array($file_status) && !empty($file_status['chunks']) && !empty($file_status['chunks'][1]['size'])) $this->chunk_size = $file_status['chunks'][1]['size']; // First, see the object's existing size (if any) $uploaded_size = $this->get_remote_size($file); try { if (1 === $updraftplus->chunked_upload($this, $file, $this->method."://".$this->container."/$file", $this->desc, $this->chunk_size, $uploaded_size)) { try { if (false !== ($data = fopen($updraftplus->backups_dir_location().'/'.$file, 'r+'))) { $this->container_object->uploadObject($file, $data); $updraftplus->log($this->desc." regular upload: success"); $updraftplus->uploaded_file($file); } else { throw new Exception('uploadObject failed: fopen failed'); } } catch (Exception $e) { $this->log("regular upload: failed ($file) (".$e->getMessage().")"); $this->log("$file: ".sprintf(__('Error: Failed to upload', 'updraftplus')), 'error'); } } } catch (Exception $e) { $updraftplus->log($this->desc.' error - failed to upload file'.' ('.$e->getMessage().') (line: '.$e->getLine().', file: '.$e->getFile().')'); $updraftplus->log(sprintf(__('%s error - failed to upload file', 'updraftplus'), $this->desc).' ('.$e->getMessage().')', 'error'); return false; } } return array('object' => $this->container_object, 'orig_path' => $opts['path'], 'container' => $this->container); } private function get_remote_size($file) { try { $response = $this->container_object->getClient()->head($this->container_object->getUrl($file))->send(); $response_object = $this->container_object->dataObject()->populateFromResponse($response)->setName($file); return $response_object->getContentLength(); } catch (Exception $e) { // Allow caller to distinguish between zero-sized and not-found return false; } } /** * This function lists the files found in the configured storage location * * @param String $match a substring to require (tested via strpos() !== false) * * @return Array - each file is represented by an array with entries 'name' and (optional) 'size' */ public function listfiles($match = 'backup_') { $opts = $this->get_options(); $container = $opts['path']; if (empty($opts['user']) || (empty($opts['apikey']) && empty($opts['password']))) return new WP_Error('no_settings', __('No settings were found', 'updraftplus')); try { $storage = $this->get_openstack_service($opts, UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts'), UpdraftPlus_Options::get_updraft_option('updraft_ssl_disableverify')); } catch (Exception $e) { return new WP_Error('no_access', sprintf(__('%s error - failed to access the container', 'updraftplus'), $this->desc).' ('.$e->getMessage().')'); } // Get the container try { $this->container_object = $storage->getContainer($container); } catch (Exception $e) { return new WP_Error('no_access', sprintf(__('%s error - failed to access the container', 'updraftplus'), $this->desc).' ('.$e->getMessage().')'); } $results = array(); $marker = ''; $page_size = 1000; try { // http://php-opencloud.readthedocs.io/en/latest/services/object-store/objects.html#list-objects-in-a-container while (null !== $marker) { $params = array( 'prefix' => $match, 'limit' => $page_size, 'marker' => $marker ); $objects = $this->container_object->objectList($params); $total = $objects->count(); if (0 == $total) break; $index = 0; while (false !== ($file = $objects->offsetGet($index)) && !empty($file)) { $index++; try { if ((is_object($file) && !empty($file->name))) { $result = array('name' => $file->name); // Rackspace returns the size of a manifested file properly; other OpenStack implementations may not if (!empty($file->bytes)) { $result['size'] = $file->bytes; } else { $size = $this->get_remote_size($file->name); if (false !== $size && $size > 0) $result['size'] = $size; } $results[] = $result; } } catch (Exception $e) { // Catch } $marker = (!empty($file->name) && $total >= $page_size) ? $file->name : null; } } } catch (Exception $e) { // Catch } return $results; } /** * Called when all chunks have been uploaded, to allow any required finishing actions to be carried out * * @param String $file - the basename of the file being uploaded * * @return Boolean - success or failure state of any finishing actions */ public function chunked_upload_finish($file) { $chunk_path = 'chunk-do-not-delete-'.$file; try { $headers = array( 'Content-Length' => 0, 'X-Object-Manifest' => sprintf('%s/%s', $this->container, $chunk_path.'_') ); $url = $this->container_object->getUrl($file); $this->container_object->getClient()->put($url, $headers)->send(); return true; } catch (Exception $e) { global $updraftplus; $updraftplus->log("Error when sending manifest (".get_class($e)."): ".$e->getMessage()); return false; } } /** * N.B. Since we use varying-size chunks, we must be careful as to what we do with $chunk_index * * @param String $file Full path for the file being uploaded * @param Resource $fp File handle to read upload data from * @param Integer $chunk_index Index of chunked upload * @param Integer $upload_size Size of the upload, in bytes * @param Integer $upload_start How many bytes into the file the upload process has got * @param Integer $upload_end How many bytes into the file we will be after this chunk is uploaded * @param Integer $total_file_size Total file size * * @return Boolean */ public function chunked_upload($file, $fp, $chunk_index, $upload_size, $upload_start, $upload_end, $total_file_size) { global $updraftplus; $file_key = 'status_'.md5($file); $file_status = $this->jobdata_get($file_key, null, 'openstack_'.$file_key); $next_chunk_size = $upload_size; $bytes_already_uploaded = 0; $last_uploaded_chunk_index = 0; // Once a chunk is uploaded, its status is set, allowing the sequence to be reconstructed if (is_array($file_status) && isset($file_status['chunks']) && !empty($file_status['chunks'])) { foreach ($file_status['chunks'] as $c_id => $c_status) { if ($c_id > $last_uploaded_chunk_index) $last_uploaded_chunk_index = $c_id; if ($chunk_index + 1 == $c_id) { $next_chunk_size = $c_status['size']; } $bytes_already_uploaded += $c_status['size']; } } else { $file_status = array('chunks' => array()); } $this->jobdata_set($file_key, $file_status); if ($upload_start < $bytes_already_uploaded) { if ($next_chunk_size != $upload_size) { $response = new stdClass; $response->new_chunk_size = $upload_size; $response->log = false; return $response; } else { return 1; } } // Shouldn't be able to happen if ($chunk_index <= $last_uploaded_chunk_index) { $updraftplus->log($this->desc.": Chunk sequence error; chunk_index=$chunk_index, last_uploaded_chunk_index=$last_uploaded_chunk_index, upload_start=$upload_start, upload_end=$upload_end, file_status=".json_encode($file_status)); } // Used to use $chunk_index here, before switching to variable chunk sizes $upload_remotepath = 'chunk-do-not-delete-'.$file.'_'.sprintf("%016d", $chunk_index); $remote_size = $this->get_remote_size($upload_remotepath); // Without this, some versions of Curl add Expect: 100-continue, which results in Curl then giving this back: curl error: 55) select/poll returned error // Didn't make the difference - instead we just check below for actual success even when Curl reports an error // $chunk_object->headers = array('Expect' => ''); if ($remote_size >= $upload_size) { $updraftplus->log($this->desc.": Chunk ($upload_start - $upload_end, $chunk_index): already uploaded"); } else { $updraftplus->log($this->desc.": Chunk ($upload_start - $upload_end, $chunk_index): begin upload"); // Upload the chunk try { $data = fread($fp, $upload_size); $time_start = microtime(true); $this->container_object->uploadObject($upload_remotepath, $data); $time_now = microtime(true); $time_taken = $time_now - $time_start; if ($next_chunk_size < 52428800 && $total_file_size > 0 && $upload_end + 1 < $total_file_size) { $job_run_time = $time_now - $updraftplus->job_time_ms; if ($time_taken < 10) { $upload_rate = $upload_size / max($time_taken, 0.0001); $upload_secs = min(floor($job_run_time), 10); if ($job_run_time < 15) $upload_secs = max(6, $job_run_time*0.6); // In megabytes $memory_limit_mb = $updraftplus->memory_check_current(); $bytes_used = memory_get_usage(); $bytes_free = $memory_limit_mb * 1048576 - $bytes_used; $new_chunk = max(min($upload_secs * $upload_rate * 0.9, 52428800, $bytes_free), 5242880); $new_chunk = $new_chunk - ($new_chunk % 5242880); $next_chunk_size = (int) $new_chunk; $updraftplus->jobdata_set('openstack_chunk_size', $next_chunk_size); } } } catch (Exception $e) { $updraftplus->log($this->desc." chunk upload: error: ($file / $chunk_index) (".$e->getMessage().") (line: ".$e->getLine().', file: '.$e->getFile().')'); // Experience shows that Curl sometimes returns a select/poll error (curl error 55) even when everything succeeded. Google seems to indicate that this is a known bug. $remote_size = $this->get_remote_size($upload_remotepath); if ($remote_size >= $upload_size) { $updraftplus->log("$file: Chunk now exists; ignoring error (presuming it was an apparently known curl bug)"); } else { $updraftplus->log("$file: ".sprintf(__('%s Error: Failed to upload', 'updraftplus'), $this->desc), 'error'); return false; } } } $file_status['chunks'][$chunk_index]['size'] = $upload_size; $this->jobdata_set($file_key, $file_status); if ($next_chunk_size != $upload_size) { $response = new stdClass; $response->new_chunk_size = $next_chunk_size; $response->log = true; return $response; } return true; } /** * Delete a single file from the service using OpenStack API * * @param Array|String $files - array of file names to delete * @param Array $data - service object and container details * @param Array $sizeinfo - unused here * @return Boolean|String - either a boolean true or an error code string */ public function delete($files, $data = false, $sizeinfo = array()) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $sizeinfo unused global $updraftplus; if (is_string($files)) $files = array($files); if (is_array($data)) { $container_object = $data['object']; $container = $data['container']; } else { $opts = $this->get_options(); $container = $opts['path']; try { $storage = $this->get_openstack_service($opts, UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts'), UpdraftPlus_Options::get_updraft_option('updraft_ssl_disableverify')); } catch (AuthenticationError $e) { $updraftplus->log($this->desc.' authentication failed ('.$e->getMessage().')'); $updraftplus->log(sprintf(__('%s authentication failed', 'updraftplus'), $this->desc).' ('.$e->getMessage().')', 'error'); return 'authentication_fail'; } catch (Exception $e) { $updraftplus->log($this->desc.' error - failed to access the container ('.$e->getMessage().')'); $updraftplus->log(sprintf(__('%s error - failed to access the container', 'updraftplus'), $this->desc).' ('.$e->getMessage().')', 'error'); return 'service_unavailable'; } // Get the container try { $container_object = $storage->getContainer($container); } catch (Exception $e) { $updraftplus->log('Could not access '.$this->desc.' container ('.get_class($e).', '.$e->getMessage().')'); $updraftplus->log(sprintf(__('Could not access %s container', 'updraftplus'), $this->desc).' ('.get_class($e).', '.$e->getMessage().')', 'error'); return 'container_access_error'; } } $ret = true; foreach ($files as $file) { $updraftplus->log($this->desc.": Delete remote: container=$container, path=$file"); // We need to search for chunks $chunk_path = "chunk-do-not-delete-".$file; try { $objects = $container_object->objectList(array('prefix' => $chunk_path)); $index = 0; while (false !== ($chunk = $objects->offsetGet($index)) && !empty($chunk)) { try { $name = $chunk->name; $container_object->dataObject()->setName($name)->delete(); $updraftplus->log($this->desc.': Chunk deleted: '.$name); } catch (Exception $e) { $updraftplus->log($this->desc." chunk delete failed: $name: ".$e->getMessage()); } $index++; } } catch (Exception $e) { $updraftplus->log($this->desc.' chunk delete failed: '.$e->getMessage()); } // Finally, delete the object itself try { $container_object->dataObject()->setName($file)->delete(); $updraftplus->log($this->desc.': Deleted: '.$file); } catch (Exception $e) { $updraftplus->log($this->desc.' delete failed: '.$e->getMessage()); $ret = 'file_delete_error'; } } return $ret; } public function download($file) { global $updraftplus; $opts = $this->get_options(); try { $storage = $this->get_openstack_service($opts, UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts'), UpdraftPlus_Options::get_updraft_option('updraft_ssl_disableverify')); } catch (AuthenticationError $e) { $updraftplus->log($this->desc.' authentication failed ('.$e->getMessage().')'); $updraftplus->log(sprintf(__('%s authentication failed', 'updraftplus'), $this->desc).' ('.$e->getMessage().')', 'error'); return false; } catch (Exception $e) { $updraftplus->log($this->desc.' error - failed to access the container ('.$e->getMessage().')'); $updraftplus->log(sprintf(__('%s error - failed to access the container', 'updraftplus'), $this->desc).' ('.$e->getMessage().')', 'error'); return false; } $container = untrailingslashit($opts['path']); $updraftplus->log($this->desc." download: ".$this->method."://$container/$file"); // Get the container try { $this->container_object = $storage->getContainer($container); } catch (Exception $e) { $updraftplus->log('Could not access '.$this->desc.' container ('.get_class($e).', '.$e->getMessage().')'); $updraftplus->log(sprintf(__('Could not access %s container', 'updraftplus'), $this->desc).' ('.get_class($e).', '.$e->getMessage().')', 'error'); return false; } // Get information about the object within the container $remote_size = $this->get_remote_size($file); if (false === $remote_size) { $updraftplus->log('Could not access '.$this->desc.' object'); $updraftplus->log(sprintf(__('The %s object was not found', 'updraftplus'), $this->desc), 'error'); return false; } return (!is_bool($remote_size)) ? $updraftplus->chunked_download($file, $this, $remote_size, true, $this->container_object) : false; } public function chunked_download($file, $headers, $container_object) { try { $dl = $container_object->getObject($file, $headers); } catch (Exception $e) { global $updraftplus; $updraftplus->log("$file: Failed to download (".$e->getMessage().")"); $updraftplus->log("$file: ".sprintf(__("%s Error", 'updraftplus'), $this->desc).": ".__('Error downloading remote file: Failed to download', 'updraftplus').' ('.$e->getMessage().")", 'error'); return false; } return $dl->getContent(); } public function credentials_test_go($opts, $path, $useservercerts, $disableverify) { if (preg_match("#^([^/]+)/(.*)$#", $path, $bmatches)) { $container = $bmatches[1]; $path = $bmatches[2]; } else { $container = $path; $path = ''; } if (empty($container)) { esc_html_e('Failure: No container details were given.', 'updraftplus'); return; } try { $storage = $this->get_openstack_service($opts, $useservercerts, $disableverify); // @codingStandardsIgnoreLine } catch (Guzzle\Http\Exception\ClientErrorResponseException $e) { $response = $e->getResponse(); $code = $response->getStatusCode(); $reason = $response->getReasonPhrase(); if (401 == $code && 'Unauthorized' == $reason) { esc_html_e('Authorisation failed (check your credentials)', 'updraftplus'); } else { echo esc_html(__('Authorisation failed (check your credentials)', 'updraftplus')." ($code:$reason)"); } return; } catch (AuthenticationError $e) { echo esc_html(sprintf(__('%s authentication failed', 'updraftplus'), $this->desc).' ('.$e->getMessage().')'); return; } catch (Exception $e) { echo esc_html(sprintf(__('%s authentication failed', 'updraftplus'), $this->desc).' ('.get_class($e).', '.$e->getMessage().')'); return; } try { $container_object = $storage->getContainer($container); // @codingStandardsIgnoreLine } catch (Guzzle\Http\Exception\ClientErrorResponseException $e) { $response = $e->getResponse(); $code = $response->getStatusCode(); $reason = $response->getReasonPhrase(); if (404 == $code) { $container_object = $storage->createContainer($container); } else { echo esc_html(__('Authorisation failed (check your credentials)', 'updraftplus')." ($code:$reason)"); return; } } catch (Exception $e) { echo esc_html(sprintf(__('%s authentication failed', 'updraftplus'), $this->desc).' ('.get_class($e).', '.$e->getMessage().')'); return; } if (!is_a($container_object, 'OpenCloud\ObjectStore\Resource\Container') && !is_a($container_object, 'Container')) { echo esc_html(sprintf(__('%s authentication failed', 'updraftplus'), $this->desc).' ('.get_class($container_object).')'); return; } $try_file = md5(rand()).'.txt'; try { $object = $container_object->uploadObject($try_file, 'UpdraftPlus test file', array('content-type' => 'text/plain')); } catch (Exception $e) { echo esc_html(sprintf(__('%s error - we accessed the container, but failed to create a file within it', 'updraftplus'), $this->desc).' ('.get_class($e).', '.$e->getMessage().')'); if (!empty($this->region)) echo ' '.esc_html(sprintf(__('Region: %s', 'updraftplus'), $this->region)); return; } echo esc_html(__('Success', 'updraftplus').": ".__('We accessed the container, and were able to create files within it.', 'updraftplus')); if (!empty($this->region)) echo ' '.esc_html(sprintf(__('Region: %s', 'updraftplus'), $this->region)); try { if (!empty($object)) { // One OpenStack server we tested on did not delete unless we slept... some kind of race condition at their end sleep(1); $object->delete(); } } catch (Exception $e) { // Catch } } /** * Get the pre configuration template * DEVELOPER NOTES: Please don't use/call this method anymore as it is currently used by OpenStack and Cloudfiles(Rackspace) storage, and it's consider to be removed in future versions. Once OpenStack and Cloudfiles templates are CSP-compliant, this should be removed and should be placed in the class child instead of the base class. * * @return String - the template */ public function get_pre_configuration_template() { global $updraftplus_admin; $classes = $this->get_css_classes(false); ?> <tr class="<?php echo esc_attr($classes . ' ' . $this->method . '_pre_config_container');?>"> <td colspan="2"> <?php if (!empty($this->img_url)) { ?> <img alt="<?php echo esc_attr($this->long_desc); ?>" src="<?php echo esc_url(UPDRAFTPLUS_URL.$this->img_url); ?>"> <?php } ?> <br> <?php // Check requirements. global $updraftplus_admin; if (!function_exists('mb_substr')) { $updraftplus_admin->show_double_warning('<strong>'.__('Warning', 'updraftplus').':</strong> '.sprintf(__('Your web server\'s PHP installation does not included a required module (%s).', 'updraftplus'), 'mbstring').' '.__('Please contact your web hosting provider\'s support.', 'updraftplus').' '.sprintf(__("UpdraftPlus's %s module <strong>requires</strong> %s.", 'updraftplus'), $this->desc, 'mbstring').' '.__('Please do not file any support requests; there is no alternative.', 'updraftplus'), $this->method); } $updraftplus_admin->curl_check($this->long_desc, false, $this->method); echo '<br>'; $this->get_pre_configuration_middlesection_template(); ?> </td> </tr> <?php } /** * Get the configuration template * DEVELOPER NOTES: Please don't use/call this method anymore as it is currently used by OpenStack and Cloudfiles(Rackspace) storage, and it's consider to be removed in future versions. Once OpenStack and Cloudfiles templates are CSP-compliant, this should be removed and should be placed in the class child instead of the base class. * * @return String - the template, ready for substitutions to be carried out */ public function get_configuration_template() { ob_start(); $template_str = ob_get_clean(); $template_str .= $this->get_configuration_middlesection_template(); $template_str .= $this->get_test_button_html($this->desc); return $template_str; } } backblaze.php 0000644 00000002257 15231511727 0007205 0 ustar 00 <?php if (!defined('UPDRAFTPLUS_DIR')) die('No direct access.'); if (version_compare(phpversion(), '5.3.3', '>=')) { if (class_exists('UpdraftPlus_Addons_RemoteStorage_backblaze')) { class UpdraftPlus_BackupModule_backblaze extends UpdraftPlus_Addons_RemoteStorage_backblaze { public function __construct() { parent::__construct('backblaze', 'Backblaze', true, true); } } } else { updraft_try_include_file('methods/addon-not-yet-present.php', 'include_once'); /** * N.B. UpdraftPlus_BackupModule_AddonNotYetPresent extends UpdraftPlus_BackupModule */ class UpdraftPlus_BackupModule_backblaze extends UpdraftPlus_BackupModule_AddonNotYetPresent { public function __construct() { parent::__construct('backblaze', 'Backblaze', '5.3.3', 'backblaze.png'); } } } } else { updraft_try_include_file('methods/insufficient.php', 'include_once'); /** * N.B. UpdraftPlus_BackupModule_insufficientphp extends UpdraftPlus_BackupModule */ class UpdraftPlus_BackupModule_backblaze extends UpdraftPlus_BackupModule_insufficientphp { public function __construct() { parent::__construct('backblaze', 'Backblaze', '5.3.3', 'backblaze.png'); } } } remotesend.php 0000644 00000057374 15231511727 0007446 0 ustar 00 <?php if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed'); if (!class_exists('UpdraftPlus_RemoteStorage_Addons_Base_v2')) updraft_try_include_file('methods/addon-base-v2.php', 'require_once'); class UpdraftPlus_BackupModule_remotesend extends UpdraftPlus_RemoteStorage_Addons_Base_v2 { private $default_chunk_size; private $remotesend_use_chunk_size; private $remotesend_chunked_wp_error; private $try_format_upgrade = false; private $remotesend_file_size; private $remotesend_uploaded_size; private $remote_sent_defchunk_transient; /** * Class constructor */ public function __construct() { // 3rd parameter: chunking? 4th: Test button? parent::__construct('remotesend', 'Remote send', false, false); } /** * Supplies the list of keys for options to be saved in the backup job. * * @return Array */ public function get_credentials() { return array('updraft_ssl_disableverify', 'updraft_ssl_nossl', 'updraft_ssl_useservercerts'); } /** * Upload a single file * * @param String $file - the basename of the file to upload * @param String $from - the full path of the file * * @return Boolean - success status. Failures can also be thrown as exceptions. */ public function do_upload($file, $from) { global $updraftplus; $opts = $this->options; try { $storage = $this->bootstrap(); if (is_wp_error($storage)) throw new Exception($storage->get_error_message()); if (!is_object($storage)) throw new Exception("RPC service error"); } catch (Exception $e) { $message = $e->getMessage().' ('.get_class($e).') (line: '.$e->getLine().', file: '.$e->getFile().')'; $this->log("RPC service error: ".$message); $this->log($message, 'error'); return false; } $filesize = filesize($from); $this->remotesend_file_size = $filesize; // See what the sending side currently has. This also serves as a ping. For that reason, we don't try/catch - we let them be caught at the next level up. $get_remote_size = $this->send_message('get_file_status', $file, 30); if (is_wp_error($get_remote_size)) { $err_msg = $get_remote_size->get_error_message(); $err_data = $get_remote_size->get_error_data(); $err_code = $get_remote_size->get_error_code(); if (!is_numeric($err_code) && isset($err_data['response']['code'])) { $err_code = $err_data['response']['code']; $err_msg = UpdraftPlus_HTTP_Error_Descriptions::get_http_status_code_description($err_code); } elseif (is_string($err_data) && preg_match('/captcha|verify.*human|turnstile/i', $err_data)) { $err_msg = __('We are unable to proceed with the process due to a bot verification requirement', 'updraftplus'); } throw new Exception($err_msg.' (get_file_status: '.$err_code.')'); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Error message to be escaped when caught and printed. } if (!is_array($get_remote_size) || empty($get_remote_size['response'])) throw new Exception(__('Unexpected response:', 'updraftplus').' '.serialize($get_remote_size)); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Error message to be escaped when caught and printed. if ('error' == $get_remote_size['response']) { $msg = $get_remote_size['data']; // Could interpret the codes to get more interesting messages directly to the user throw new Exception(__('Error:', 'updraftplus').' '.$msg); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Error message to be escaped when caught and printed. } if (empty($get_remote_size['data']) || !isset($get_remote_size['data']['size']) || 'file_status' != $get_remote_size['response']) throw new Exception(__('Unexpected response:', 'updraftplus').' '.serialize($get_remote_size)); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Error message to be escaped when caught and printed. // Possible statuses: 0=temporary file (or not present), 1=file if (empty($get_remote_size['data'])) { $remote_size = 0; $remote_status = 0; } else { $remote_size = (int) $get_remote_size['data']['size']; $remote_status = $get_remote_size['data']['status']; } $this->log("$file: existing size: ".$remote_size); // Perhaps it already exists? (if we didn't get the final confirmation) if ($remote_size >= $filesize && $remote_status) { $this->log("$file: already uploaded"); return true; } // Length = 44 (max = 45) $this->remote_sent_defchunk_transient = 'ud_rsenddck_'.md5($opts['name_indicator']); if (empty($this->default_chunk_size)) { // Default is 2MB. After being b64-encoded twice, this is ~ 3.7MB = 113 seconds on 32KB/s uplink $default_chunk_size = $updraftplus->jobdata_get('clone_job') ? 4194304 : 2097152; if (defined('UPDRAFTPLUS_REMOTESEND_DEFAULT_CHUNK_BYTES') && UPDRAFTPLUS_REMOTESEND_DEFAULT_CHUNK_BYTES >= 16384) $default_chunk_size = UPDRAFTPLUS_REMOTESEND_DEFAULT_CHUNK_BYTES; $this->default_chunk_size = $default_chunk_size; } $default_chunk_size = $this->default_chunk_size; if (false !== ($saved_default_chunk_size = get_transient($this->remote_sent_defchunk_transient)) && $saved_default_chunk_size > 16384) { // Don't go lower than 256KB for the *default*. (The job size can go lower). $default_chunk_size = max($saved_default_chunk_size, 262144); } $this->remotesend_use_chunk_size = $updraftplus->jobdata_get('remotesend_chunksize', $default_chunk_size); if (0 == $remote_size && $this->remotesend_use_chunk_size == $this->default_chunk_size && $updraftplus->current_resumption - max($updraftplus->jobdata_get('uploaded_lastreset'), 1) > 1) { $new_chunk_size = floor($this->remotesend_use_chunk_size / 2); $this->log("No uploading activity has been detected for a while; reducing chunk size in case a timeout was occurring. New chunk size: ".$new_chunk_size); $this->remotesend_set_new_chunk_size($new_chunk_size); } try { if (false != ($handle = fopen($from, 'rb'))) { $this->remotesend_uploaded_size = $remote_size; $ret = $updraftplus->chunked_upload($this, $file, $this->method."://".trailingslashit($opts['url']).$file, $this->description, $this->remotesend_use_chunk_size, $remote_size, true); fclose($handle); return $ret; } else { throw new Exception("Failed to open file for reading: $from"); } } catch (Exception $e) { $this->log("upload: error (".get_class($e)."): ($file) (".$e->getMessage().") (line: ".$e->getLine().', file: '.$e->getFile().')'); if (!empty($this->remotesend_chunked_wp_error) && is_wp_error($this->remotesend_chunked_wp_error)) { $this->log("Exception data: ".base64_encode(serialize($this->remotesend_chunked_wp_error->get_error_data()))); } return false; } return true; } /** * Chunked upload * * @param String $file Specific file to be used in chunked upload * @param Resource $fp File handle * @param Integer $chunk_index The index of the chunked data * @param Integer $upload_size Size of the upload * @param Integer $upload_start String the upload starts on * @param Integer $upload_end String the upload ends on * * @return Boolean|Integer Result (N.B> (int)1 means the same as true, but additionally indicates "don't log it") */ public function chunked_upload($file, $fp, $chunk_index, $upload_size, $upload_start, $upload_end) { // Condition used to be "$upload_start < $this->remotesend_uploaded_size" - but this assumed that the other side never failed after writing only some bytes to disk // $upload_end is the byte offset of the final byte. Therefore, add 1 onto it when comparing with a size. if ($upload_end + 1 <= $this->remotesend_uploaded_size) return 1; global $updraftplus; $chunk = fread($fp, $upload_size); if (false === $chunk) { $this->log("upload: $file: fread failure ($upload_start)"); return false; } $try_again = false; $data = array('file' => $file, 'data' => base64_encode($chunk), 'start' => $upload_start); if ($upload_end+1 >= $this->remotesend_file_size) { $data['last_chunk'] = true; if ('' != ($label = $updraftplus->jobdata_get('label'))) $data['label'] = $label; } // ~ 3.7MB of data typically - timeout allows for 15.9KB/s try { $put_chunk = $this->send_message('send_chunk', $data, 240); } catch (Exception $e) { $try_again = true; } if ($try_again || is_wp_error($put_chunk)) { // 413 = Request entity too large // Don't go lower than 64KB chunks (i.e. 128KB/2) // Note that mod_security can be configured to 'helpfully' decides to replace HTTP error codes + messages with a simple serving up of the site home page, which means that we need to also guess about other reasons this condition may have occurred other than detecting via the direct 413 code. Of course, our search for wp-includes|wp-content|WordPress|/themes/ would be thwarted by someone who tries to hide their WP. The /themes/ is pretty hard to hide, as the theme directory is always <wp-content-dir>/themes - even if you moved your wp-content. The point though is just a 'best effort' - this doesn't have to be infallible. if (is_wp_error($put_chunk)) { $error_data = $put_chunk->get_error_data(); $is_413 = ('unexpected_http_code' == $put_chunk->get_error_code() && ( 413 == $error_data || (is_array($error_data) && !empty($error_data['response']['code']) && 413 == $error_data['response']['code']) ) ); $is_timeout = ('http_request_failed' == $put_chunk->get_error_code() && false !== strpos($put_chunk->get_error_message(), 'timed out')); if ($this->remotesend_use_chunk_size >= 131072 && ($is_413 || $is_timeout || ('response_not_understood' == $put_chunk->get_error_code() && (false !== strpos($error_data, 'wp-includes') || false !== strpos($error_data, 'wp-content') || false !== strpos($error_data, 'WordPress') || false !== strpos($put_chunk->get_error_data(), '/themes/'))))) { if (1 == $chunk_index) { $new_chunk_size = floor($this->remotesend_use_chunk_size / 2); $this->remotesend_set_new_chunk_size($new_chunk_size); $log_msg = "Returned WP_Error: code=".$put_chunk->get_error_code(); if ('unexpected_http_code' == $put_chunk->get_error_code()) $log_msg .= ' ('.$error_data.')'; $log_msg .= " - reducing chunk size to: ".$new_chunk_size; $this->log($log_msg); return new WP_Error('reduce_chunk_size', 'HTTP 413 or possibly equivalent condition on first chunk - should reduce chunk size', $new_chunk_size); } elseif ($this->remotesend_use_chunk_size >= 131072 && $is_413) { // In this limited case, where we got a 413 but the chunk is not number 1, our algorithm/architecture doesn't allow us to just resume immediately with a new chunk size. However, we can just have UD reduce the chunk size on its next resumption. $new_chunk_size = floor($this->remotesend_use_chunk_size / 2); $this->remotesend_set_new_chunk_size($new_chunk_size); $log_msg = "Returned WP_Error: code=".$put_chunk->get_error_code().", message=".$put_chunk->get_error_message(); $log_msg .= " - reducing chunk size to: ".$new_chunk_size." and then scheduling resumption/aborting"; $this->log($log_msg); UpdraftPlus_Job_Scheduler::reschedule(50); UpdraftPlus_Job_Scheduler::record_still_alive(); die; } } } $put_chunk = $this->send_message('send_chunk', $data, 240); } if (is_wp_error($put_chunk)) { // The exception handler is within this class. So we can store the data. $this->remotesend_chunked_wp_error = $put_chunk; throw new Exception($put_chunk->get_error_message().' ('.$put_chunk->get_error_code().')'); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Error message to be escaped when caught and printed. } if (!is_array($put_chunk) || empty($put_chunk['response'])) throw new Exception(__('Unexpected response:', 'updraftplus').' '.serialize($put_chunk)); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Error message to be escaped when caught and printed. if ('error' == $put_chunk['response']) { $msg = $put_chunk['data']; // Could interpret the codes to get more interesting messages directly to the user // The textual prefixes here were added after 1.12.5 - hence optional when parsing if (preg_match('/^invalid_start_too_big:(start=)?(\d+),(existing_size=)?(\d+)/', $msg, $matches)) { $existing_size = $matches[2]; if ($existing_size < $this->remotesend_uploaded_size) { // The file on the remote system seems to have shrunk. Could be some load-balancing system with a distributed filesystem that is only eventually consistent. return new WP_Error('try_again', 'File on remote system is smaller than expected - perhaps an eventually-consistent filesystem (wait and retry)'); } } throw new Exception(__('Error:', 'updraftplus').' '.$msg); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Error message to be escaped when caught and printed. } if ('file_status' != $put_chunk['response']) throw new Exception(__('Unexpected response:', 'updraftplus').' '.serialize($put_chunk)); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Error message to be escaped when caught and printed. // Possible statuses: 0=temporary file (or not present), 1=file if (empty($put_chunk['data']) || !is_array($put_chunk['data'])) { $this->log("Unexpected response when putting chunk $chunk_index: ".serialize($put_chunk)); return false; } else { $remote_size = (int) $put_chunk['data']['size']; $this->remotesend_uploaded_size = $remote_size; } return true; } /** * This function will send a message to the remote site to inform it that the backup has finished sending, on success will update the jobdata key upload_completed and return true else false * * @return Boolean - returns true on success or false on error, all errors are logged to the backup log */ public function upload_completed() { global $updraftplus; $service = $updraftplus->jobdata_get('service'); $remote_sent = (!empty($service) && ((is_array($service) && in_array('remotesend', $service)) || 'remotesend' === $service)); if (!$remote_sent) return; // If this is a partial upload then don't send the upload complete signal if ('partialclouduploading' == $updraftplus->jobdata_get('jobstatus')) return; // ensure options have been loaded $this->options = $this->get_options(); try { $storage = $this->bootstrap(); if (is_wp_error($storage)) throw new Exception($storage->get_error_message()); if (!is_object($storage)) throw new Exception("RPC service error"); } catch (Exception $e) { $message = $e->getMessage().' ('.get_class($e).') (line: '.$e->getLine().', file: '.$e->getFile().')'; $this->log("RPC service error: ".$message); $this->log($message, 'error'); return false; } if (is_wp_error($storage)) return $updraftplus->log_wp_error($storage, false, true); for ($i = 0; $i < 3; $i++) { $success = false; $response = $this->send_message('upload_complete', array('job_id' => $updraftplus->nonce), 30); if (is_wp_error($response)) { $message = $response->get_error_message().' (upload_complete: '.$response->get_error_code().')'; $this->log("RPC service error: ".$message); $this->log($message, 'error'); } elseif (!is_array($response) || empty($response['response'])) { $this->log("RPC service error: ".serialize($response)); $this->log(serialize($response), 'error'); } elseif ('error' == $response['response']) { // Could interpret the codes to get more interesting messages directly to the user $msg = $response['data']; $this->log("RPC service error: ".$msg); $this->log($msg, 'error'); } elseif ('file_status' == $response['response']) { $success = true; break; } sleep(5); } return $success; } /** * Change the chunk size * * @param Integer $new_chunk_size - in bytes */ private function remotesend_set_new_chunk_size($new_chunk_size) { global $updraftplus; $this->remotesend_use_chunk_size = $new_chunk_size; $updraftplus->jobdata_set('remotesend_chunksize', $new_chunk_size); // Save, so that we don't have to cycle through the illegitimate/larger chunk sizes each time. Set the transient expiry to 120 days, in case they change hosting/configuration - so that we're not stuck on the lower size forever. set_transient($this->remote_sent_defchunk_transient, $new_chunk_size, 86400*120); } /** * Send a message to the remote site * * @param String $message - the message identifier * @param Array|Null $data - the data to send with the message * @param Integer $timeout - timeout in waiting for a response * * @return Array|WP_Error - results, or an error */ private function send_message($message, $data = null, $timeout = 30) { $storage = $this->get_storage(); if (is_array($this->try_format_upgrade) && is_array($data)) { $data['sender_public'] = $this->try_format_upgrade['local_public']; } $response = $storage->send_message($message, $data, $timeout); if (is_array($response) && !empty($response['data']) && is_array($response['data'])) { if (!empty($response['data']['php_events']) && !empty($response['data']['previous_data'])) { foreach ($response['data']['php_events'] as $logline) { $this->log("From remote side: ".$logline); } $response['data'] = $response['data']['previous_data']; } if (is_array($response) && is_array($response['data']) && !empty($response['data']['got_public'])) { $name_indicator = $this->try_format_upgrade['name_indicator']; $remotesites = UpdraftPlus_Options::get_updraft_option('updraft_remotesites'); foreach ($remotesites as $key => $site) { if (!is_array($site) || empty($site['name_indicator']) || $site['name_indicator'] != $name_indicator) continue; // This means 'format 2' $this->try_format_upgrade = true; $remotesites[$key]['remote_got_public'] = 1; // If this DB save fails, they'll have to recreate the key UpdraftPlus_Options::update_updraft_option('updraft_remotesites', $remotesites); // Now we need to get a fresh storage object, because the remote end will no longer accept messages with format=1 $this->set_storage(null); $this->do_bootstrap(null); break; } } } return $response; } public function do_bootstrap($opts, $connect = true) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $connect unused global $updraftplus; $updraftplus->ensure_phpseclib(); if (!class_exists('UpdraftPlus_Remote_Communications_V2')) include_once(apply_filters('updraftplus_class_udrpc_path', UPDRAFTPLUS_DIR.'/vendor/team-updraft/common-libs/src/updraft-rpc/class-udrpc2.php', $updraftplus->version)); $opts = $this->get_opts(); try { $ud_rpc = new UpdraftPlus_Remote_Communications_V2($opts['name_indicator']); if (!empty($opts['format_support']) && 2 == $opts['format_support'] && !empty($opts['local_private']) && !empty($opts['local_public']) && !empty($opts['remote_got_public'])) { $ud_rpc->set_message_format(2); $ud_rpc->set_key_remote($opts['key']); $ud_rpc->set_key_local($opts['local_private']); } else { // Enforce the legacy communications protocol (which is only suitable for when only one side only sends, and the other only receives - which is what we happen to do) $ud_rpc->set_message_format(1); $ud_rpc->set_key_local($opts['key']); if (!empty($opts['format_support']) && 2 == $opts['format_support'] && !empty($opts['local_public']) && !empty($opts['local_private'])) { $this->try_format_upgrade = array('name_indicator' => $opts['name_indicator'], 'local_public' => $opts['local_public']); } } $ud_rpc->set_destination_url($opts['url']); $ud_rpc->activate_replay_protection(); } catch (Exception $e) { return new WP_Error('rpc_failure', "Commmunications failure: ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); } do_action('updraftplus_remotesend_udrpc_object_obtained', $ud_rpc, $opts); $this->set_storage($ud_rpc); return $ud_rpc; } public function options_exist($opts) { if (is_array($opts) && !empty($opts['url']) && !empty($opts['name_indicator']) && !empty($opts['key'])) return true; return false; } public function get_opts() { global $updraftplus; $opts = $updraftplus->jobdata_get('remotesend_info'); $opts = $this->clone_remotesend_options($opts); if (true === $this->try_format_upgrade && is_array($opts)) $opts['remote_got_public'] = 1; return is_array($opts) ? $opts : array(); } /** * This function will check the options we have for the remote send and if it's a clone job and there are missing settings it will call the mothership to get this information. * * @param Array $opts - an array of remote send options * * @return Array - an array of options */ public function clone_remotesend_options($opts) { // Don't call self::log() - this then requests options (to get the label), causing an infinite loop. global $updraftplus, $updraftplus_admin; if (empty($updraftplus_admin)) updraft_try_include_file('admin.php', 'include_once'); $clone_job = $updraftplus->jobdata_get('clone_job'); // check this is a clone job before we proceed if (empty($clone_job)) return $opts; // check that we don't already have the needed information if (is_array($opts) && !empty($opts['url']) && !empty($opts['name_indicator']) && !empty($opts['key'])) return $opts; $current_stage = $updraftplus->jobdata_get('jobstatus'); $updraftplus->jobdata_set('jobstatus', 'clonepolling'); $clone_id = $updraftplus->jobdata_get('clone_id'); $clone_url = $updraftplus->jobdata_get('clone_url'); $clone_key = $updraftplus->jobdata_get('clone_key'); $secret_token = $updraftplus->jobdata_get('secret_token'); $clone_region = $updraftplus->jobdata_get('clone_region'); if (empty($clone_id) && empty($secret_token)) return $opts; $updraftplus->log("Polling for UpdraftClone (ID: {$clone_id} Region: {$clone_region}) migration information."); $params = array('clone_id' => $clone_id, 'secret_token' => $secret_token); $response = $updraftplus->get_updraftplus_clone()->clone_info_poll($params); if (!isset($response['status']) || 'success' != $response['status']) { if ('clone_network_not_found' == $response['code'] && 0 === $updraftplus->current_resumption) { $updraftplus->log("UpdraftClone network information is not ready yet please wait while the clone finishes provisioning."); } else { $updraftplus->log("UpdraftClone migration information poll failed with code: " . $response['code']); } return $opts; } if (!isset($response['data']) || !isset($response['data']['url']) || !isset($response['data']['key'])) { $updraftplus->log("UpdraftClone migration information poll unexpected return information with code:" . $response['code']); return $opts; } $clone_url = $response['data']['url']; $clone_key = json_decode($response['data']['key'], true); if (empty($clone_url) || empty($clone_key)) { $updraftplus->log("UpdraftClone migration information not found (probably still provisioning): will poll again in 60"); UpdraftPlus_Job_Scheduler::reschedule(60); UpdraftPlus_Job_Scheduler::record_still_alive(); die; } // Store the information $remotesites = UpdraftPlus_Options::get_updraft_option('updraft_remotesites'); if (!is_array($remotesites)) $remotesites = array(); foreach ($remotesites as $k => $rsite) { if (!is_array($rsite)) continue; if ($rsite['url'] == $clone_key['url']) unset($remotesites[$k]); } $remotesites[] = $clone_key; UpdraftPlus_Options::update_updraft_option('updraft_remotesites', $remotesites); $updraftplus->jobdata_set_multi('clone_url', $clone_url, 'clone_key', $clone_key, 'remotesend_info', $clone_key, 'jobstatus', $current_stage); return $clone_key; } // do_listfiles(), do_download(), do_delete() : the absence of any method here means that the parent will correctly throw an error } backup-module.php 0000644 00000100334 15231511727 0010012 0 ustar 00 <?php if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed.'); abstract class UpdraftPlus_BackupModule { private $_options; private $_instance_id; private $_storage; /** * Store options (within this class) for this remote storage module. There is also a parameter for saving to the permanent storage (i.e. database). * * @param array $options array of options to store * @param Boolean $save whether or not to also save the options to the database * @param null|String $instance_id optionally set the instance ID for this instance at the same time. This is required if you have not already set an instance ID with set_instance_id() * @return void|Boolean If saving to DB, then the result of the DB save operation is returned. */ public function set_options($options, $save = false, $instance_id = null) { $this->_options = $options; // Remove any previously-stored storage object, because this is usually tied to the options if (!empty($this->_storage)) unset($this->_storage); if ($instance_id) $this->set_instance_id($instance_id); if ($save) return $this->save_options(); } /** * Saves the current options to the database. This is a private function; external callers should use set_options(). * * @throws Exception if trying to save options without indicating an instance_id, or if the remote storage module does not have the multi-option capability */ private function save_options() { if (!$this->supports_feature('multi_options')) { throw new Exception('save_options() can only be called on a storage method which supports multi_options (this module, '.$this->get_id().', does not)'); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Error message to be escaped when caught and printed. } if (!$this->_instance_id) { throw new Exception('save_options() requires an instance ID, but was called without setting one (either directly or via set_instance_id())'); } $current_db_options = UpdraftPlus_Storage_Methods_Interface::update_remote_storage_options_format($this->get_id()); if (is_wp_error($current_db_options)) { throw new Exception('save_options(): options fetch/update failed ('.$current_db_options->get_error_code().': '.$current_db_options->get_error_message().')'); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Error message to be escaped when caught and printed. } $current_db_options['settings'][$this->_instance_id] = $this->_options; return UpdraftPlus_Options::update_updraft_option('updraft_'.$this->get_id(), $current_db_options); } /** * Retrieve default options for this remote storage module. * This method would normally be over-ridden by the child. * * @return Array - an array of options */ public function get_default_options() { return array(); } /** * Retrieve persistent class variables and/or methods (the ones that don't get changed during runtime) and transform them into a list of template properties * * @return Array an associative array keyed by names of the corresponding variables/methods, the keys might not exactly be the same with the name of the variables/methods */ protected function get_persistent_variables_and_methods() { global $updraftplus; return array( 'css_class' => 'updraftplusmethod', 'is_multi_options_feature_supported' => $this->supports_feature('multi_options'), 'is_config_templates_feature_supported' => $this->supports_feature('config_templates'), 'is_conditional_logic_feature_supported' => $this->supports_feature('conditional_logic'), 'is_multi_servers_feature_supported' => $this->supports_feature('multi_servers'), 'method_id' => $this->get_id(), '_instance_id' => $this->_instance_id, 'method_display_name' => $updraftplus->backup_methods[$this->get_id()], 'admin_page_url' => UpdraftPlus_Options::admin_page_url(), 'storage_auth_nonce' =>wp_create_nonce('storage_auth_nonce'), 'input_select_folder_label' => __('Select existing folder', 'updraftplus'), 'input_confirm_label' => __('Confirm', 'updraftplus'), 'input_cancel_label' => __('Cancel', 'updraftplus'), ); } /** * Get all persistent variables and methods across the modules (this could mean the child including its parent), also the necessary required HTML element attributes and texts which are unique to each child * NOTE: Since this method would normally be over-ridden by the child, please sanitise all strings that are required to be shown as HTML content on the frontend side (i.e. wp_kses()) * * @return Array an associative array keyed by names that describe themselves as they are */ public function get_template_properties() { return array(); } /** * List all allowed HTML tags for content sanitisation * * @return Array an associatve array keyed by name of the allowed HTML tags */ protected function allowed_html_for_content_sanitisation() { return array( 'a' => array( 'href' => array(), 'title' => array(), 'target' => array(), ), 'br' => array(), 'em' => array(), 'strong' => array(), 'p' => array(), 'div' => array( 'class' => array(), ), 'kbd' => array(), ); } /** * Get partial templates associated to the corresponding backup module (remote storage object) * N.B. This method would normally be over-ridden by the child. * * @return Array an associative array keyed by name of the partial templates */ public function get_partial_templates() { return array(); } /** * Check whether options have been set up by the user, or not * This method would normally be over-ridden by the child. * * @param Array $opts - the potential options * * @return Boolean */ public function options_exist($opts) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Unused parameter is present because the caller uses 1 argument and normally this method will be over-ridden by the child class. return false; } /** * Retrieve a list of supported features for this storage method * This method should be over-ridden by methods supporting new * features. * * Keys are strings, and values are booleans. * * Currently known features: * * - multi_options : indicates that the remote storage module * can handle its options being in the Feb-2017 multi-options * format. N.B. This only indicates options handling, not any * other multi-destination options. * * - multi_servers : not implemented yet: indicates that the * remote storage module can handle multiple servers at backup * time. This should not be specified without multi_options. * multi_options without multi_servers is fine - it will just * cause only the first entry in the options array to be used. * * - config_templates : not implemented yet: indicates that * the remote storage module can output its configuration in * Handlebars format via the get_configuration_template() method. * * - conditional_logic : indicates that the remote storage module * can handle predefined logics regarding how backups should be * sent to the remote storage * * @return Array - an array of supported features (any features not * mentioned are assumed to not be supported) */ public function get_supported_features() { return array(); } /** * This method should only be called if the feature 'multi storage' is supported. In that case, it returns a template with information about the remote storage. The code below is a placeholder, and methods supporting the feature should always over-ride it. * * @return String - HTML template */ public function get_pre_configuration_template() { return $this->get_id().": called, but not implemented in the child class (coding error)"; } /** * This method should only be called if the feature 'config templates' is supported. In that case, it returns a template with appropriate placeholders for specific settings. The code below is a placeholder, and methods supporting the feature should always over-ride it. * * @return String - HTML template */ public function get_configuration_template() { return $this->get_id().": called, but not implemented in the child class (coding error)"; } /** * This method will set the stored storage object to that indicated * * @param Object $storage - the storage client */ public function set_storage($storage) { $this->_storage = $storage; } /** * This method will return the stored storage client * * @return Object - the stored remote storage client */ public function get_storage() { if (!empty($this->_storage)) return $this->_storage; } /** * Outputs id and name fields, as if currently within an input tag * * This assumes standardised options handling (i.e. that the options array is updraft_(method-id)) * * @param Array|String $field - the field identifiers * @param Boolean $return_instead_of_echo - tells the method if it should return the output or echo it to page */ public function output_settings_field_name_and_id($field, $return_instead_of_echo = false) { $method_id = $this->get_id(); $instance_id = $this->supports_feature('config_templates') ? '{{instance_id}}' : $this->_instance_id; $id = ''; $name = ''; if (is_array($field)) { foreach ($field as $value) { $id .= '_'.$value; $name .= '['.$value.']'; } } else { $id = '_'.$field; $name = '['.$field.']'; } $output = "id=\"updraft_{$method_id}{$id}_{$instance_id}\" name=\"updraft_{$method_id}[settings][{$instance_id}]{$name}\" "; if ($return_instead_of_echo) { return $output; } else { echo wp_kses_post($output); } } /** * Get the CSS ID * * @param String $field - the field identifier to return a CSS ID for * * @return String */ public function get_css_id($field) { $method_id = $this->get_id(); $instance_id = $this->supports_feature('config_templates') ? '{{instance_id}}' : $this->_instance_id; return "updraft_{$method_id}_{$field}_{$instance_id}"; } /** * Get handlebarsjs template * This deals with any boiler-plate, prior to calling config_print() * * @uses self::config_print() * @uses self::get_configuration_template() * * return handlebarsjs template or html */ public function get_template() { ob_start(); // Allow methods to not use this hidden field, if they do not output any settings (to prevent their saved settings being over-written by just this hidden field) if ($this->print_shared_settings_fields()) { ?><tr class="<?php echo esc_attr($this->get_css_classes()); ?>"><input type="hidden" name="updraft_<?php echo esc_attr($this->get_id());?>[version]" value="1"></tr><?php } if ($this->supports_feature('config_templates')) { ?> {{#if first_instance}} <?php $this->get_pre_configuration_template(); if ($this->supports_feature('multi_storage')) { do_action('updraftplus_config_print_add_multi_storage', $this->get_id(), $this); } ?> {{/if}} <?php do_action('updraftplus_config_print_before_storage', $this->get_id(), $this); if ('updraftvault' !== $this->get_id()) do_action('updraftplus_config_print_add_conditional_logic', $this->get_id(), $this); if ($this->supports_feature('multi_storage')) { do_action('updraftplus_config_print_add_instance_label', $this->get_id(), $this); } $template = ob_get_clean(); $template .= $this->get_configuration_template(); if ('updraftvault' === $this->get_id()) { ob_start(); do_action('updraftplus_config_print_add_conditional_logic', $this->get_id(), $this); $template .= ob_get_clean(); } } else { do_action('updraftplus_config_print_before_storage', $this->get_id(), $this); do_action('updraftplus_config_print_add_conditional_logic', $this->get_id(), $this); // N.B. These are mutually exclusive: config_print() is not used if config_templates is supported. So, even during transition, the UpdraftPlus_BackupModule instance only needs to support one of the two, not both. $this->config_print(); $template = ob_get_clean(); } return $template; } /** * Modifies handerbar template options. Other child class can extend it. * * @param array $opts * @return Array - Modified handerbar template options */ public function transform_options_for_template($opts) { return $opts; } /** * Gives settings keys which values should not passed to handlebarsjs context. * The settings stored in UD in the database sometimes also include internal information that it would be best not to send to the front-end (so that it can't be stolen by a man-in-the-middle attacker) * * @return Array - Settings array keys which should be filtered */ public function filter_frontend_settings_keys() { return array(); } /** * Over-ride this to allow methods to not use the hidden version field, if they do not output any settings (to prevent their saved settings being over-written by just this hidden field * * @return [boolean] - return true to output the version field or false to not output the field */ public function print_shared_settings_fields() { return true; } /** * Prints out the configuration section for a particular module. This is now (Sep 2017) considered deprecated; things are being ported over to get_configuration_template(), indicated via the feature 'config_templates'. */ public function config_print() { echo esc_html($this->get_id()).": module neither declares config_templates support, nor has a config_print() method (coding bug)"; } /** * Supplies the list of keys for options to be saved in the backup job. * * @return Array */ public function get_credentials() { $keys = array('updraft_ssl_disableverify', 'updraft_ssl_nossl', 'updraft_ssl_useservercerts'); if (!$this->supports_feature('multi_servers')) $keys[] = 'updraft_'.$this->get_id(); return $keys; } /** * Returns a space-separated list of CSS classes suitable for rows in the configuration section * * @param Boolean $include_instance - a boolean value to indicate if we want to include the instance_id in the css class, we may not want to include the instance if it's for a UI element that we don't want to be removed along with other UI elements that do include a instance id. * * @returns String - the list of CSS classes */ public function get_css_classes($include_instance = true) { $classes = 'updraftplusmethod '.$this->get_id(); if (!$include_instance) return $classes; if ($this->supports_feature('multi_options')) { if ($this->supports_feature('config_templates')) { $classes .= ' '.$this->get_id().'-{{instance_id}}'; } else { $classes .= ' '.$this->get_id().'-'.$this->_instance_id; } } return $classes; } /** * * Returns HTML for a row for a test button * * @param String $title - The text to be used in the button * * @returns String - The HTML to be inserted into the settings page */ protected function get_test_button_html($title) { ob_start(); $instance_id = $this->supports_feature('config_templates') ? '{{instance_id}}' : $this->_instance_id; ?> <tr class="<?php echo esc_attr($this->get_css_classes()); ?>"> <th></th> <td><p><button id="updraft-<?php echo esc_attr($this->get_id());?>-test-<?php echo esc_attr($instance_id);?>" type="button" class="button-primary updraft-test-button updraft-<?php echo esc_attr($this->get_id());?>-test" data-instance_id="<?php echo esc_attr($instance_id);?>" data-method="<?php echo esc_attr($this->get_id());?>" data-method_label="<?php echo esc_attr($title);?>"><?php echo esc_html(sprintf(__('Test %s Settings', 'updraftplus'), $title));?></button></p></td> </tr> <?php return ob_get_clean(); } /** * Get the backup method identifier for this class * * @return String - the identifier */ public function get_id() { $class = get_class($this); // UpdraftPlus_BackupModule_ return substr($class, 25); } /** * Get the backup method description for this class * * @return String - the identifier */ public function get_description() { global $updraftplus; $methods = $updraftplus->backup_methods; $id = $this->get_id(); return isset($methods[$id]) ? $methods[$id] : $id; } /** * Sets the instance ID - for supporting multi_options * * @param String $instance_id - the instance ID */ public function set_instance_id($instance_id) { $this->_instance_id = $instance_id; } /** * Sets the instance ID - for supporting multi_options * * @returns String the instance ID */ public function get_instance_id() { return $this->_instance_id; } /** * Check whether this storage module supports a mentioned feature * * @param String $feature - the feature concerned * * @returns Boolean */ public function supports_feature($feature) { return in_array($feature, $this->get_supported_features()); } /** * Retrieve options for this remote storage module. * N.B. The option name instance_id is reserved and should not be used. * * @uses get_default_options * * @return Array - array of options. This will include default values for any options not set. */ public function get_options() { global $updraftplus; $supports_multi_options = $this->supports_feature('multi_options'); if (is_array($this->_options)) { // First, prioritise any options that were explicitly set. This is the eventual goal for all storage modules. $options = $this->_options; } elseif (is_callable(array($this, 'get_opts'))) { // Next, get any options available via a legacy / over-ride method. if ($supports_multi_options) { // This is forbidden, because get_opts() is legacy and is for methods that do not support multi-options. Supporting multi-options leads to the array format being updated, which will then break get_opts(). die('Fatal error: method '.esc_html($this->get_id()).' both supports multi_options and provides a get_opts method'); } $options = $this->get_opts(); } else { // Next, look for job options (which in turn, falls back to saved settings if no job options were set) $options = $updraftplus->get_job_option('updraft_'.$this->get_id()); if (!is_array($options)) $options = array(); if ($supports_multi_options) { if (!isset($options['version'])) { $options_full = UpdraftPlus_Storage_Methods_Interface::update_remote_storage_options_format($this->get_id()); if (is_wp_error($options_full)) { $updraftplus->log("Options retrieval failure: ".$options_full->get_error_code().": ".$options_full->get_error_message()." (".json_encode($options_full->get_error_data()).")"); return array(); } } else { $options_full = $options; } // UpdraftPlus_BackupModule::get_options() is for getting the current instance's options. So, this branch (going via the job option) is a legacy route, and hence we just give back the first one. The non-legacy route is to call the set_options() method externally. $options = (isset($options_full['settings']) && is_array($options_full['settings'])) ? reset($options_full['settings']) : false; if (false === $options) { $updraftplus->log("Options retrieval failure (no options set)"); return array(); } $instance_id = key($options_full['settings']); $this->set_options($options, false, $instance_id); } } $options = apply_filters( 'updraftplus_backupmodule_get_options', wp_parse_args($options, $this->get_default_options()), $this ); return $options; } /** * Set job data that is local to this storage instance * (i.e. the key does not need to be unique across instances) * * @uses UpdraftPlus::jobdata_set() * * @param String $key - the key for the job data * @param Mixed $value - the data to be stored */ public function jobdata_set($key, $value) { $instance_key = $this->get_id().'-'.($this->_instance_id ? $this->_instance_id : 'no_instance'); global $updraftplus; $instance_data = $updraftplus->jobdata_get($instance_key); if (!is_array($instance_data)) $instance_data = array(); $instance_data[$key] = $value; $updraftplus->jobdata_set($instance_key, $instance_data); } /** * Get job data that is local to this storage instance * (i.e. the key does not need to be unique across instances) * * @uses UpdraftPlus::jobdata_get() * * @param String $key - the key for the job data * @param Mixed $default - the default to return if nothing was set * @param String|Null $legacy_key - the previous name of the key, prior to instance-specific job data (so that upgrades across versions whilst a backup is in progress can still find its data). In future, support for this can be removed. */ public function jobdata_get($key, $default = null, $legacy_key = null) { $instance_key = $this->get_id().'-'.($this->_instance_id ? $this->_instance_id : 'no_instance'); global $updraftplus; $instance_data = $updraftplus->jobdata_get($instance_key); if (is_array($instance_data) && isset($instance_data[$key])) return $instance_data[$key]; return is_string($legacy_key) ? $updraftplus->jobdata_get($legacy_key, $default) : $default; } /** * Delete job data that is local to this storage instance * (i.e. the key does not need to be unique across instances) * * @uses UpdraftPlus::jobdata_set() * * @param String $key - the key for the job data * @param String|Null $legacy_key - the previous name of the key, prior to instance-specific job data (so that upgrades across versions whilst a backup is in progress can still find its data) */ public function jobdata_delete($key, $legacy_key = null) { $instance_key = $this->get_id().'-'.($this->_instance_id ? $this->_instance_id : 'no_instance'); global $updraftplus; $instance_data = $updraftplus->jobdata_get($instance_key); if (is_array($instance_data) && isset($instance_data[$key])) { unset($instance_data[$key]); $updraftplus->jobdata_set($instance_key, $instance_data); } if (is_string($legacy_key)) $updraftplus->jobdata_delete($legacy_key); } /** * This method will either return or echo the constructed auth link for the remote storage method * * @param Boolean $echo_instead_of_return - a boolean to indicate if the authentication link should be echo or returned * @param Boolean $template_instead_of_notice - a boolean to indicate if the authentication link is for a template or a notice * @return Void|String - returns a string or nothing depending on the parameters */ public function get_authentication_link($echo_instead_of_return = true, $template_instead_of_notice = true) { if (!$echo_instead_of_return) { ob_start(); } $account_warning = ''; $description = $this->get_description(); if ($this->output_account_warning()) { $account_warning = __('Ensure you are logged into the correct account before continuing.', 'updraftplus'); } if ($template_instead_of_notice) { $instance_id = "{{instance_id}}"; $text = sprintf(__("<strong>After</strong> you have saved your settings (by clicking 'Save Changes' below), then come back here and follow this link to complete authentication with %s.", 'updraftplus'), $description); } else { $instance_id = $this->get_instance_id(); $text = sprintf(__('Follow this link to authorize access to your %s account (you will not be able to backup to %s without it).', 'updraftplus'), $description, $description); } echo esc_html($account_warning) . ' ' . wp_kses_post($this->build_authentication_link($instance_id, $text)); if (!$echo_instead_of_return) { return ob_get_clean(); } } /** * This function will build and return the authentication link * * @param String $instance_id - the instance id * @param String $text - the link text * * @return String - the authentication link */ public function build_authentication_link($instance_id, $text) { $id = $this->get_id(); if (!preg_match('/^[-A-Z0-9]+$/i', $instance_id)) return ''; return '<a class="updraft_authlink" href="'.UpdraftPlus_Options::admin_page_url().'?&action=updraftmethod-'.$id.'-auth&page=updraftplus&updraftplus_'.$id.'auth=doit&nonce='.wp_create_nonce('storage_auth_nonce').'&updraftplus_instance='.$instance_id.'" data-instance_id="'.$instance_id.'" data-remote_method="'.$id.'">'.$text.'</a>'; } /** * Check the authentication is valid before proceeding to call the authentication method */ public function action_authenticate_storage() { if (isset($_GET['updraftplus_'.$this->get_id().'auth']) && 'doit' == $_GET['updraftplus_'.$this->get_id().'auth'] && !empty($_GET['updraftplus_instance']) && preg_match('/^[-A-Z0-9]+$/i', $_GET['updraftplus_instance']) && isset($_GET['nonce']) && wp_verify_nonce($_GET['nonce'], 'storage_auth_nonce')) { $this->authenticate_storage((string) $_GET['updraftplus_instance']); } } /** * Authenticate the remote storage and save settings * * @param String $instance_id - The remote storage instance id */ public function authenticate_storage($instance_id) { if (method_exists($this, 'do_authenticate_storage')) { $this->do_authenticate_storage($instance_id); } else { error_log($this->get_id().": module does not have an authenticate storage method (coding bug)"); } } /** * This method will either return or echo the constructed deauth link for the remote storage method * * @param Boolean $echo_instead_of_return - a boolean to indicate if the deauthentication link should be echo or returned * @return Void|String - returns a string or nothing depending on the parameters */ public function get_deauthentication_link($echo_instead_of_return = true) { if (!$echo_instead_of_return) { ob_start(); } $id = $this->get_id(); $description = $this->get_description(); echo ' <a class="updraft_deauthlink" href="'.esc_url(UpdraftPlus_Options::admin_page_url().'?action=updraftmethod-'.$id.'-auth&page=updraftplus&updraftplus_'.$id.'auth=deauth&nonce='.wp_create_nonce($id.'_deauth_nonce').'&updraftplus_instance={{instance_id}}').'" data-instance_id="{{instance_id}}" data-remote_method="'.esc_attr($id).'">'.esc_html(sprintf(__("Follow this link to remove these settings for %s.", 'updraftplus'), $description)).'</a>'; if (!$echo_instead_of_return) { return ob_get_clean(); } } /** * Check the deauthentication is valid before proceeding to call the deauthentication method */ public function action_deauthenticate_storage() { if (isset($_GET['updraftplus_'.$this->get_id().'auth']) && 'deauth' == $_GET['updraftplus_'.$this->get_id().'auth'] && !empty($_GET['nonce']) && !empty($_GET['updraftplus_instance']) && preg_match('/^[-A-Z0-9]+$/i', $_GET['updraftplus_instance']) && wp_verify_nonce($_GET['nonce'], $this->get_id().'_deauth_nonce')) { $this->deauthenticate_storage($_GET['updraftplus_instance']); } } /** * Deauthenticate the remote storage and remove the saved settings * * @param String $instance_id - The remote storage instance id */ public function deauthenticate_storage($instance_id) { if (method_exists($this, 'do_deauthenticate_storage')) { $this->do_deauthenticate_storage($instance_id); } $opts = $this->get_default_options(); $this->set_options($opts, true, $instance_id); } /** * Get the manual authorisation template * * @return String - the template */ public function get_manual_authorisation_template() { $id = $this->get_id(); $description = $this->get_description(); $template = "<div id='updraftplus_manual_authorisation_template_{$id}'>"; $template .= "<strong>".sprintf(__('%s authentication:', 'updraftplus'), $description)."</strong>"; $template .= "<p>".sprintf(__('If you are having problems authenticating with %s you can manually authorize here.', 'updraftplus'), $description)."</p>"; $template .= "<p>".__('To complete manual authentication, at the orange UpdraftPlus authentication screen select the "Having problems authenticating?" link, then copy and paste the code given here.', 'updraftplus')."</p>"; $template .= "<label for='updraftplus_manual_authentication_data_{$id}'>".sprintf(__('%s authentication code:', 'updraftplus'), $description)."</label> <input type='text' id='updraftplus_manual_authentication_data_{$id}' name='updraftplus_manual_authentication_data_{$id}'>"; $template .= "<p id='updraftplus_manual_authentication_error_{$id}'></p>"; $template .= "<button type='button' data-method='{$id}' class='button button-primary' id='updraftplus_manual_authorisation_submit_{$id}'>".__('Complete manual authentication', 'updraftplus')."</button>"; $template .= '<span class="updraftplus_spinner spinner">' . __('Processing', 'updraftplus') . '...</span>'; $template .= "</div>"; return $template; } /** * This will call the remote storage methods complete authentication function * * @param string $state - the remote storage authentication state * @param string $code - the remote storage authentication code * * @return String - returns a string response */ public function complete_authentication($state, $code) { if (method_exists($this, 'do_complete_authentication')) { return $this->do_complete_authentication($state, $code, true); } else { $message = $this->get_id().": module does not have an complete authentication method (coding bug)"; error_log($message); return $message; } } /** * Over-ride this to allow methods to output extra information about using the correct account for OAuth storage methods * * @return Boolean - return false so that no extra information is output */ public function output_account_warning() { return false; } /** * This function is a wrapper and will call $updraftplus->log(), the backup modules should use this so we can add information to the log lines to do with the remote storage and instance settings. * * @param string $line - the log line * @param string $level - the log level: notice, warning, error. If suffixed with a hyphen and a destination, then the default destination is changed too. * @param boolean $uniq_id - each of these will only be logged once * @param boolean $skip_dblog - if true, then do not write to the database * * @return void */ public function log($line, $level = 'notice', $uniq_id = false, $skip_dblog = false) { global $updraftplus; $prefix = $this->get_storage_label(); $updraftplus->log("$prefix: $line", $level, $uniq_id, $skip_dblog); } /** * Log appropriate messages for a multi-delete response. * * @param Array $files * @param Array $responses - using the same keys as $files * * @return Boolean - true if no errors were found, otherwise false */ protected function process_multi_delete_responses($files, $responses) { global $updraftplus; $ret = true; if (is_array($responses)) { foreach ($responses as $key => $response) { if ('success' == $response) { $updraftplus->log("$files[$key]: Delete succeeded"); } elseif (is_array($response)) { $ret = false; if (isset($response['error']) && isset($response['error']['code']) && isset($response['error']['message'])) { $updraftplus->log("Delete failed for file: $files[$key] with error code: ".$response['error']['code']." message: ".$response['error']['message']); } else { $updraftplus->log("Delete failed for file: $files[$key]"); } } } } elseif (!$responses) { $ret = false; $updraftplus->log("Delete failed for files: ".implode($files)); } return $ret; } /** * This function will build and return the remote storage instance label * * @return String - the remote storage instance label */ private function get_storage_label() { $opts = $this->get_options(); $label = isset($opts['instance_label']) ? $opts['instance_label'] : ''; $description = $this->get_description(); if (!empty($label)) { $prefix = (false !== strpos($label, $description)) ? $label : "$description: $label"; } else { $prefix = $description; } return $prefix; } /** * This method will output any needed js for the JSTree. * * @return void */ public function admin_footer_jstree() { static $script_output = array(); // Static array to store script output status. $id = $this->get_id(); // Check if the script has already been output for this ID. if (!isset($script_output[$id])) { wp_add_inline_script('updraft-admin-common', "var js_tree_".esc_js($id)." = new updraft_js_tree('".esc_js($id)."'); js_tree_".esc_js($id).".init();", 'after'); // Mark the script as output for this ID. $script_output[$id] = true; } } } s3.php 0000644 00000203117 15231511727 0005612 0 ustar 00 <?php if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed.'); // Converted to multi-options (Feb 2017-) and previous options conversion removed: Yes /** * This class is used by both UpdraftPlus_S3 and UpdraftPlus_S3_Compat */ class UpdraftPlus_S3Exception extends Exception { public function __construct($message, $file, $line, $code = 0) { parent::__construct($message, $code); $this->file = $file; $this->line = $line; } } if (!class_exists('UpdraftPlus_BackupModule')) updraft_try_include_file('methods/backup-module.php', 'require_once'); class UpdraftPlus_BackupModule_s3 extends UpdraftPlus_BackupModule { // This variable can/should be over-ridden in child classes as appropriate protected $use_v4 = true; // This variable can/should be over-ridden in child classes as appropriate protected $provider_can_use_aws_sdk = true; // This variable can/should be over-ridden in child classes as appropriate protected $provider_has_regions = true; protected $quota_used = null; protected $s3_exception; protected $download_chunk_size = 10485760; protected $current_upload_entity = 'files'; private $got_with; /** * Retrieve specific options for this remote storage module * * @param Boolean $force_refresh - if set, and if relevant, don't use cached credentials, but get them afresh * * @return Array - an array of options */ protected function get_config($force_refresh = false) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $force_refresh unused $opts = $this->get_options(); $opts['whoweare'] = 'S3'; $opts['whoweare_long'] = 'Amazon S3'; $opts['key'] = 's3'; return $opts; } /** * This method overrides the parent method and lists the supported features of this remote storage option. * * @return Array - an array of supported features (any features not mentioned are asuumed to not be supported) */ public function get_supported_features() { // This options format is handled via only accessing options via $this->get_options() return array('multi_options', 'config_templates', 'multi_storage', 'conditional_logic'); } /** * Retrieve default options for this remote storage module. * * @return Array - an array of options */ public function get_default_options() { return array( 'accesskey' => '', 'secretkey' => '', 'path' => '', 'rrs' => '', 'server_side_encryption' => '', ); } /** * Return the name of the class that should be used to interface with the storage provider, and make sure that it has been include()-ed. * * @return String - e.g. UpdraftPlus_S3, UpdraftPlus_S3_Compat */ protected function indicate_s3_class() { // N.B. : The classes must have different names, as if multiple remote storage options are chosen, then we could theoretically need both (if both Amazon and a compatible-S3 provider are used) // UpdraftPlus_S3 (the internal, non-AWS toolkit) is used when not accessing Amazon Web Services, so set that as the default $class_to_use = 'UpdraftPlus_S3'; // If on a PHP version supported by the AWS SDK, and if the constant forcing the internal toolkit is not set, then consider using it. The 3.x branch of the AWS SDK requires PHP 5.5+ // The "old" in the define is a legacy reference to a time before v4 signatures were in that library. if (version_compare(PHP_VERSION, '5.5', '>=') && $this->provider_can_use_aws_sdk && (!defined('UPDRAFTPLUS_S3_OLDLIB') || !UPDRAFTPLUS_S3_OLDLIB)) { // Switch to AWS SDK increasingly less over time if (!function_exists('curl_init') || (defined('UPDRAFTPLUS_FORCE_S3_AWS_SDK') && UPDRAFTPLUS_FORCE_S3_AWS_SDK) || apply_filters('updraftplus_indicate_s3_class_prefer_aws_sdk', false)) { $class_to_use = 'UpdraftPlus_S3_Compat'; } } if (!class_exists($class_to_use)) { if ('UpdraftPlus_S3_Compat' == $class_to_use) { updraft_try_include_file('includes/S3compat.php', 'include_once'); } else { updraft_try_include_file('includes/S3.php', 'include_once'); } } return $class_to_use; } /** * Get an S3 object, after setting our options. Public because called externally from UpdraftPlus_Addon_S3_Enhanced * * @param String $key S3 Key * @param String $secret S3 secret * @param Boolean $useservercerts User server certificates * @param Boolean $disableverify Check if disableverify is enabled * @param Boolean $nossl Check if there is SSL or not * @param Null|String $endpoint S3 endpoint to use * @param Boolean $sse A flag to use server side encryption * @param String $session_token The session token returned by AWS for temporary credentials access * * @return Object|WP_Error */ public function getS3($key, $secret, $useservercerts, $disableverify, $nossl, $endpoint = null, $sse = false, $session_token = null) { $storage = $this->get_storage(); if (!empty($storage) && !is_wp_error($storage)) return $storage; if (is_string($key)) $key = trim($key); if (is_string($secret)) $secret = trim($secret); // Ignore the 'nossl' setting if the endpoint is DigitalOcean Spaces (https://developers.digitalocean.com/documentation/v2/) if (is_string($endpoint) && preg_match('/\.digitaloceanspaces\.com$/i', $endpoint)) { $nossl = apply_filters('updraftplus_gets3_nossl', false, $endpoint, $nossl); } // Saved in case the object needs recreating for the corner-case where there is no permission to look up the bucket location $this->got_with = array( 'key' => $key, 'secret' => $secret, 'useservercerts' => $useservercerts, 'disableverify' => $disableverify, 'nossl' => $nossl, 'server_side_encryption' => $sse, 'session_token' => $session_token ); if (is_wp_error($key)) return $key; if ('' == $key || '' == $secret) { return new WP_Error('no_settings', get_class($this).': '.__('No settings were found - please go to the Settings tab and check your settings', 'updraftplus')); } $use_s3_class = $this->indicate_s3_class(); if (!class_exists('WP_HTTP_Proxy')) include_once(ABSPATH.WPINC.'/class-http.php'); $proxy = new WP_HTTP_Proxy(); $use_ssl = true; $ssl_ca = false; try { $storage = new $use_s3_class($key, $secret, $use_ssl, $ssl_ca, $endpoint, $session_token); // Comment before 1.22.10 - "the use of calling use_dns_bucket_name method here is to switch the storage from using path-style to dns style." But at this stage, we don't have a bucket name, so this shouldn't be here. // $this->maybe_use_dns_bucket_name($storage, ...); $signature_version = empty($this->use_v4) ? 'v2' : 'v4'; $signature_version = apply_filters('updraftplus_s3_signature_version', $signature_version, $this->use_v4, $this); if (!is_a($storage, 'UpdraftPlus_S3_Compat')) { $storage->setSignatureVersion($signature_version); } } catch (Exception $e) { // Catch a specific PHP engine bug - see HS#6364 if ('UpdraftPlus_S3_Compat' == $use_s3_class && is_a($e, 'InvalidArgumentException') && false !== strpos('Invalid signature type: s3', $e->getMessage())) { updraft_try_include_file('includes/S3.php', 'include_once'); $use_s3_class = 'UpdraftPlus_S3'; $try_again = true; } else { $this->log(__('Error: Failed to initialise', 'updraftplus').": ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); $this->log(__('Error: Failed to initialise', 'updraftplus')); return new WP_Error('s3_init_failed', sprintf(__('%s Error: Failed to initialise', 'updraftplus'), 'S3').": ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); } } if (!empty($try_again)) { try { $storage = new $use_s3_class($key, $secret, $use_ssl, $ssl_ca, $endpoint, $session_token); // Comment before 1.22.10 - "the use of calling use_dns_bucket_name method here is to switch the storage from using path-style to dns style." But at this stage, we don't have a bucket name, so this shouldn't be here. // $this->maybe_use_dns_bucket_name($storage, ...); } catch (Exception $e) { $this->log(__('Error: Failed to initialise', 'updraftplus').": ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); $this->log(__('Error: Failed to initialise', 'updraftplus')); return new WP_Error('s3_init_failed', sprintf(__('%s Error: Failed to initialise', 'updraftplus'), 'S3').": ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); } $this->log("Hit a PHP engine bug - had to switch to the internal S3 library"); } if ($proxy->is_enabled()) { // WP_HTTP_Proxy returns empty strings where we want nulls $user = $proxy->username(); if (empty($user)) { $user = null; $pass = null; } else { $pass = $proxy->password(); if (empty($pass)) $pass = null; } $port = (int) $proxy->port(); if (empty($port)) $port = 8080; $storage->setProxy($proxy->host(), $user, $pass, CURLPROXY_HTTP, $port); } if ($this->use_sse() || $sse) $storage->setServerSideEncryption('AES256'); $this->set_storage($storage); return $storage; } /** * Given an S3 object, possibly set the region on it * * @param Object $obj - like UpdraftPlus_S3 * @param String $region * @param String $bucket_name */ protected function set_region($obj, $region, $bucket_name = '') {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $bucket_name // AWS Regions: https://docs.aws.amazon.com/general/latest/gr/rande.html // https://docs.aws.amazon.com/general/latest/gr/s3.html#auto-endpoints-s3 switch ($region) { case 'EU': case 'eu-west-1': $endpoint = 's3-eu-west-1.amazonaws.com'; break; case 'us-east-1': $endpoint = 's3.amazonaws.com'; break; case 'us-west-1': case 'us-east-2': case 'us-west-2': $endpoint = 's3-'.$region.'.amazonaws.com'; break; case 'sa-east-1': case 'us-gov-west-1': case 'ca-central-1': case 'ap-northeast-1': case 'ap-northeast-2': case 'ap-southeast-1': case 'ap-southeast-2': case 'eu-central-1': case 'eu-south-1': case 'eu-north-1': case 'eu-west-2': case 'eu-west-3': case 'ap-south-1': case 'me-south-1': case 'af-south-1': case 'ap-east-1': case 'ap-northeast-3': case 'ap-south-2': case 'ap-southeast-3': case 'ap-southeast-5': case 'ap-southeast-4': case 'ap-southeast-7': case 'ca-west-1': case 'eu-south-2': case 'eu-central-2': case 'il-central-1': case 'me-central-1': case 'us-gov-east-1': $endpoint = 's3.'.$region.'.amazonaws.com'; break; case 'cn-north-1': case 'cn-northwest-1': $endpoint = 's3.'.$region.'.amazonaws.com.cn'; break; default: break; } if (isset($endpoint)) { $this->log("Set region (".get_class($obj)."): $region"); $obj->setRegion($region); if (!is_a($obj, 'UpdraftPlus_S3_Compat')) { $this->log("Set endpoint: $endpoint"); $obj->setEndpoint($endpoint); } } } /** * Whether to always use server-side encryption. * * This can be over-ridden in child classes of course... and the method here is both the default and the value used for AWS * * @return Boolean */ protected function use_sse() { return false; } /** * Perform the upload of backup archives * * @param Array $backup_array - a list of file names (basenames) (within UD's directory) to be uploaded * * @return Mixed - return (boolean)false to indicate failure, or anything else to have it passed back at the delete stage (most useful for a storage object). */ public function backup($backup_array) { global $updraftplus; $config = $this->get_config(); if (empty($config['accesskey']) && !empty($config['error_message'])) { $err = new WP_Error('no_settings', $config['error_message']); return $updraftplus->log_wp_error($err, false, true); } if (empty($config['sessiontoken'])) $config['sessiontoken'] = null; $storage = $this->getS3( $config['accesskey'], $config['secretkey'], UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts'), UpdraftPlus_Options::get_updraft_option('updraft_ssl_disableverify'), UpdraftPlus_Options::get_updraft_option('updraft_ssl_nossl'), null, !empty($config['server_side_encryption']), $config['sessiontoken'] ); if (is_wp_error($storage)) return $updraftplus->log_wp_error($storage, false, true); $whoweare = $config['whoweare']; $whoweare_key = $config['key']; $whoweare_keys = substr($whoweare_key, 0, 3); if (is_a($storage, 'UpdraftPlus_S3_Compat') && !class_exists('XMLWriter')) { $this->log('The required XMLWriter PHP module is not installed'); $this->log(sprintf(__('The required %s PHP module is not installed - ask your web hosting company to enable it', 'updraftplus'), 'XMLWriter'), 'error'); return false; } $bucket_name = untrailingslashit($config['path']); $bucket_path = ""; $orig_bucket_name = $bucket_name; if (preg_match("#^([^/]+)/(.*)$#", $bucket_name, $bmatches)) { $bucket_name = $bmatches[1]; $bucket_path = $bmatches[2]."/"; } list($storage, $config, $bucket_exists, $region) = $this->get_bucket_access($storage, $config, $bucket_name, $bucket_path); // See if we can detect the region (which implies the bucket exists and is ours), or if not create it if ($bucket_exists) { $updraft_dir = trailingslashit($updraftplus->backups_dir_location()); foreach ($backup_array as $key => $file) { if ('db' != substr($key, 0, 2)) { $this->current_upload_entity = 'files'; } else { $this->current_upload_entity = 'databases'; } // We upload in 5MB chunks to allow more efficient resuming and hence uploading of larger files // N.B.: 5MB is Amazon's minimum. So don't go lower or you'll break it. $fullpath = $updraft_dir.$file; $orig_file_size = filesize($fullpath); if (!file_exists($fullpath)) { $this->log("File not found: $file: $whoweare: "); $this->log("$file: ".sprintf(__('Error: %s', 'updraftplus'), __('File not found', 'updraftplus')), 'error'); continue; } if (isset($config['quota']) && method_exists($this, 's3_get_quota_info')) { $quota_used = $this->s3_get_quota_info('numeric', $config['quota']); if (false === $quota_used) { $this->log("Quota usage: count failed"); } else { $this->quota_used = $quota_used; if ($config['quota'] - $this->quota_used < $orig_file_size) { if (method_exists($this, 's3_out_of_quota')) call_user_func(array($this, 's3_out_of_quota'), $config['quota'], $this->quota_used, $orig_file_size); continue; } else { $quota_transient_used = $this->quota_transient_used ? ' (via transient)' : ''; // We don't need to log this always - the s3_out_of_quota method will do its own logging $this->log("Quota is available: used=$quota_used (".round($quota_used/1048576, 1)." MB), total=".$config['quota']." (".round($config['quota']/1048576, 1)." MB), needed=$orig_file_size (".round($orig_file_size/1048576, 1)." MB)".$quota_transient_used); } } } $chunks = floor($orig_file_size / 5242880); // There will be a remnant unless the file size was exactly on a 5MB boundary if ($orig_file_size % 5242880 > 0) $chunks++; $hash = md5($file); $extra = $this->provider_has_regions ? " ($region)" : ''; $this->log("upload{$extra}: $file (chunks: $chunks) -> $whoweare_key://$bucket_name/$bucket_path$file"); $filepath = $bucket_path.$file; // This is extra code for the 1-chunk case, but less overhead (no bothering with job data) if ($chunks < 2) { $storage->setExceptions(true); try { if (!$storage->putObjectFile($fullpath, $bucket_name, $filepath, 'private', array(), array(), apply_filters('updraft_'.$whoweare_key.'_storageclass', 'STANDARD', $storage, $config))) { $this->log("regular upload: failed ($fullpath)"); $this->log("$file: ".sprintf(__('%s Error: Failed to upload', 'updraftplus'), $whoweare), 'error'); } else { $this->quota_used += $orig_file_size; if (method_exists($this, 's3_record_quota_info')) $this->s3_record_quota_info($this->quota_used, $config['quota']); $extra_log = ''; if (method_exists($this, 's3_get_quota_info')) { $extra_log = ', quota used now: '.round($this->quota_used / 1048576, 1).' MB'; } $this->log("regular upload: success$extra_log"); $updraftplus->uploaded_file($file); } } catch (Exception $e) { $this->log("$file: ".sprintf(__('%s Error: Failed to upload', 'updraftplus'), $whoweare).": ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile()); $this->log("$file: ".sprintf(__('%s Error: Failed to upload', 'updraftplus'), $whoweare), 'error'); } $storage->setExceptions(false); } else { // Retrieve the upload ID $upload_id = $this->jobdata_get($hash.'_uid', null, "upd_{$whoweare_keys}_{$hash}_uid"); if (empty($upload_id)) { $storage->setExceptions(true); try { $upload_id = $storage->initiateMultipartUpload($bucket_name, $filepath, 'private', array(), array(), apply_filters('updraft_'.$whoweare_key.'_storageclass', 'STANDARD', $storage, $config)); } catch (Exception $e) { $this->log("exception (".get_class($e).") whilst trying initiateMultipartUpload: ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); if (false !== strpos($e->getMessage(), '[ExpiredToken]')) { $this->log("Re-scheduling resumption and aborting so that new token can be requested upon resumption"); UpdraftPlus_Job_Scheduler::reschedule(50); UpdraftPlus_Job_Scheduler::record_still_alive(); die; } $upload_id = false; } $storage->setExceptions(false); if (empty($upload_id)) { $this->log("upload: failed: could not get uploadId for multipart upload ($filepath)"); $this->log(sprintf(__("%s upload: getting uploadID for multipart upload failed - see log file for more details", 'updraftplus'), $whoweare), 'error'); continue; } else { $this->log("chunked upload: got multipart ID: $upload_id"); $this->jobdata_set($hash.'_uid', $upload_id); } } else { $this->log("chunked upload: retrieved previously obtained multipart ID: $upload_id"); } $successes = 0; $etags = array(); for ($i = 1; $i <= $chunks; $i++) { $etag = $this->jobdata_get($hash.'_etag_'.$i, null, "ud_{$whoweare_keys}_{$hash}_e$i"); if (null !== $etag && strlen($etag) > 0) { $this->log("chunk $i: was already completed (etag: $etag)"); $successes++; array_push($etags, $etag); } else { // Sanity check: we've seen a case where an overlap was truncating the file from underneath us if (filesize($fullpath) < $orig_file_size) { $this->log("error: $key: chunk $i: file was truncated underneath us (orig_size=$orig_file_size, now_size=".filesize($fullpath).")"); $this->log(sprintf(__('error: file %s was shortened unexpectedly', 'updraftplus'), $fullpath), 'error'); } $storage->setExceptions(true); try { $etag = $storage->uploadPart($bucket_name, $filepath, $upload_id, $fullpath, $i); } catch (Exception $e) { $this->log("exception (".get_class($e).") during uploadPart: ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); if (false !== strpos($e->getMessage(), '[ExpiredToken]')) { $this->log("Re-scheduling resumption and aborting so that new token can be requested upon resumption"); UpdraftPlus_Job_Scheduler::reschedule(50); UpdraftPlus_Job_Scheduler::record_still_alive(); die; } $etag = false; } $storage->setExceptions(false); if (false !== $etag && is_string($etag)) { $updraftplus->record_uploaded_chunk(round(100*$i/$chunks, 1), "$i, $etag", $fullpath); array_push($etags, $etag); $this->jobdata_set($hash.'_etag_'.$i, $etag); $successes++; } else { $this->log("chunk $i: upload failed"); $this->log(sprintf(__("chunk %s: upload failed", 'updraftplus'), $i), 'error'); } } } if ($successes >= $chunks) { $this->log("upload: all chunks uploaded; will now instruct $whoweare to re-assemble"); $storage->setExceptions(true); try { if ($storage->completeMultipartUpload($bucket_name, $filepath, $upload_id, $etags)) { $this->log("upload ($key): re-assembly succeeded"); $updraftplus->uploaded_file($file); $this->quota_used += $orig_file_size; if (method_exists($this, 's3_record_quota_info')) $this->s3_record_quota_info($this->quota_used, $config['quota']); } else { $this->log("upload ($key): re-assembly failed ($file)"); $this->log(sprintf(__('upload (%s): re-assembly failed (see log for more details)', 'updraftplus'), $key), 'error'); } } catch (Exception $e) { $this->log("re-assembly error ($key): ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); $this->log($e->getMessage().": ".sprintf(__('%s re-assembly error (%s): (see log file for more)', 'updraftplus'), $whoweare, $e->getMessage()), 'error'); } // Remember to unset, as the deletion code later reuses the object $storage->setExceptions(false); } else { $this->log("upload: upload was not completely successful on this run"); } } } // Allows counting of the final quota accurately if (method_exists($this, 's3_prune_retained_backups_finished')) { add_action('updraftplus_prune_retained_backups_finished', array($this, 's3_prune_retained_backups_finished')); } return array('storage' => $storage, 's3_orig_bucket_name' => $orig_bucket_name); } else { $extra_text = empty($this->s3_exception) ? '' : ' '.$this->s3_exception->getMessage().' (line: '.$this->s3_exception->getLine().', file: '.$this->s3_exception->getFile().')'; $extra_text_short = empty($this->s3_exception) ? '' : ' '.$this->s3_exception->getMessage(); $this->log("Error: Failed to access bucket $bucket_name.".$extra_text); $this->log(sprintf(__('Error: Failed to access bucket %s.', 'updraftplus'), $bucket_name).' '.__('Check your permissions and credentials.', 'updraftplus').' (1)'.$extra_text_short, 'error'); } } /** * This function lists the files found in the configured storage location * * @param String $match - a substring to require (tested via strpos() !== false) * * @return Array - each file is represented by an array with entries 'name' and (optional) 'size' */ public function listfiles($match = 'backup_') { $config = $this->get_config(); return $this->listfiles_with_path($config['path'], $match); } protected function possibly_wait_for_bucket_or_user($config, $storage) { $bucket_name = untrailingslashit($config['path']); if (preg_match("#^([^/]+)/(.*)$#", $bucket_name, $bmatches)) { $bucket_name = $bmatches[1]; } if (!empty($config['is_new_bucket'])) { if (method_exists($storage, 'waitForBucket')) { $storage->setExceptions(true); try { $storage->waitForBucket($bucket_name); } catch (Exception $e) { // This seems to often happen - we get a 403 on a newly created user/bucket pair, even though the bucket was already waited for by the creator // We could just sleep() - a sleep(5) seems to do it. However, given that it's a new bucket, that's unnecessary. $storage->setExceptions(false); return array(); } $storage->setExceptions(false); } else { sleep(4); } } elseif (!empty($config['is_new_user'])) { // A crude waiter, because the AWS toolkit does not have one for IAM propagation - basically, loop around a few times whilst the access attempt still fails $attempt_flag = 0; while ($attempt_flag < 5) { $attempt_flag++; if (@$storage->getBucketLocation($bucket_name)) {// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise because of the method. $attempt_flag = 100; } else { if (empty($config['sessiontoken'])) $config['sessiontoken'] = null; sleep($attempt_flag*1.5 + 1); $sse = !empty($config['server_side_encryption']); // Get the bucket object again... because, for some reason, the AWS PHP SDK (at least on the current version we're using, March 2016) calculates an incorrect signature on subsequent attempts $this->set_storage(null); $storage = $this->getS3( $config['accesskey'], $config['secretkey'], UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts'), UpdraftPlus_Options::get_updraft_option('updraft_ssl_disableverify'), UpdraftPlus_Options::get_updraft_option('updraft_ssl_nossl'), null, $sse, $config['sessiontoken'] ); if (is_wp_error($storage)) return $storage; if (!is_a($storage, 'UpdraftPlus_S3') && !is_a($storage, 'UpdraftPlus_S3_Compat')) return new WP_Error('no_s3object', 'Failed to gain access to '.$config['whoweare']); } } } return $storage; } /** * The purpose of splitting this into a separate method, is to also allow listing with a different path * * @param String $path Path to check * @param String $match The match for idetifying the bucket name * @param Boolean $include_subfolders Check if list file need to include sub folders * @return Array */ public function listfiles_with_path($path, $match = 'backup_', $include_subfolders = false) { $bucket_name = untrailingslashit($path); $bucket_path = ''; if (preg_match("#^([^/]+)/(.*)$#", $bucket_name, $bmatches)) { $bucket_name = $bmatches[1]; $bucket_path = trailingslashit($bmatches[2]); } $config = $this->get_config(); $sse = empty($config['server_side_encryption']) ? false : true; if (empty($config['sessiontoken'])) $config['sessiontoken'] = null; $storage = $this->getS3( $config['accesskey'], $config['secretkey'], UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts'), UpdraftPlus_Options::get_updraft_option('updraft_ssl_disableverify'), UpdraftPlus_Options::get_updraft_option('updraft_ssl_nossl'), null, $sse, $config['sessiontoken'] ); if (is_wp_error($storage)) return $storage; if (!is_a($storage, 'UpdraftPlus_S3') && !is_a($storage, 'UpdraftPlus_S3_Compat')) return new WP_Error('no_s3object', 'Failed to gain access to '.$config['whoweare']); $storage = $this->possibly_wait_for_bucket_or_user($config, $storage); if (!is_a($storage, 'UpdraftPlus_S3') && !is_a($storage, 'UpdraftPlus_S3_Compat')) return $storage; list($storage, $config, $bucket_exists, $region) = $this->get_bucket_access($storage, $config, $bucket_name, $bucket_path);// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- The value is passed back from get_bucket_access $bucket = $storage->getBucket($bucket_name, $bucket_path.$match); if (!is_array($bucket)) return array(); $results = array(); foreach ($bucket as $key => $object) { if (!is_array($object) || empty($object['name'])) continue; if (isset($object['size']) && 0 == $object['size']) continue; if ($bucket_path) { if (0 !== strpos($object['name'], $bucket_path)) continue; $object['name'] = substr($object['name'], strlen($bucket_path)); } else { if (!$include_subfolders && false !== strpos($object['name'], '/')) continue; } $result = array('name' => $object['name']); if (isset($object['size'])) $result['size'] = (int) $object['size']; unset($bucket[$key]); $results[] = $result; } return $results; } /** * Delete a single file from the service using S3 * * @param Array|String $files - array of file names to delete * @param Array $s3arr - s3 service object and container details * @param Array $sizeinfo - size of files to delete, used for quota calculation * @return Boolean|String - either a boolean true or an error code string */ public function delete($files, $s3arr = false, $sizeinfo = array()) { global $updraftplus; if (is_string($files)) $files = array($files); $config = $this->get_config(); $sse = empty($config['server_side_encryption']) ? false : true; if (empty($config['sessiontoken'])) $config['sessiontoken'] = null; if ($s3arr) { $storage = $s3arr['storage']; $orig_bucket_name = $s3arr['s3_orig_bucket_name']; } else { $storage = $this->getS3( $config['accesskey'], $config['secretkey'], UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts'), UpdraftPlus_Options::get_updraft_option('updraft_ssl_disableverify'), UpdraftPlus_Options::get_updraft_option('updraft_ssl_nossl'), null, $sse, $config['sessiontoken'] ); if (is_wp_error($storage)) return $updraftplus->log_wp_error($storage, false, false); $bucket_name = untrailingslashit($config['path']); $orig_bucket_name = $bucket_name; if (preg_match("#^([^/]+)/(.*)$#", $bucket_name, $bmatches)) { $bucket_name = $bmatches[1]; $bucket_path = $bmatches[2]."/"; } else { $bucket_path = ''; } list($storage, $config, $bucket_exists, $region) = $this->get_bucket_access($storage, $config, $bucket_name, $bucket_path);// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- The value is passed back from get_bucket_access if (!$bucket_exists) { $this->log("Error: Failed to access bucket $bucket_name. Check your permissions and credentials."); $this->log(sprintf(__('Error: Failed to access bucket %s.', 'updraftplus'), $bucket_name).' '.__('Check your permissions and credentials.', 'updraftplus'), 'error'); return 'container_access_error'; } } $ret = true; foreach ($files as $i => $file) { if (preg_match("#^([^/]+)/(.*)$#", $orig_bucket_name, $bmatches)) { $s3_bucket=$bmatches[1]; $s3_uri = $bmatches[2]."/".$file; } else { $s3_bucket = $orig_bucket_name; $s3_uri = $file; } $this->log("Delete remote: bucket=$s3_bucket, URI=$s3_uri"); $storage->setExceptions(true); try { if (!$storage->deleteObject($s3_bucket, $s3_uri)) { $this->log("Delete failed"); } elseif (null !== $this->quota_used && !empty($sizeinfo[$i]) && isset($config['quota']) && method_exists($this, 's3_record_quota_info')) { $this->quota_used -= $sizeinfo[$i]; $this->s3_record_quota_info($this->quota_used, $config['quota']); } } catch (Exception $e) { $this->log("delete failed (".get_class($e)."): ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); $storage->setExceptions(false); $ret = 'file_delete_error'; } $storage->setExceptions(false); } return $ret; } /** * Download a file from the remote storage * * @param string $file The specific file to be downloaded * @return void */ public function download($file) { global $updraftplus; $config = $this->get_config(); $sse = empty($config['server_side_encryption']) ? false : true; if (empty($config['sessiontoken'])) $config['sessiontoken'] = null; $storage = $this->getS3( $config['accesskey'], $config['secretkey'], UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts'), UpdraftPlus_Options::get_updraft_option('updraft_ssl_disableverify'), UpdraftPlus_Options::get_updraft_option('updraft_ssl_nossl'), null, $sse, $config['sessiontoken'] ); if (is_wp_error($storage)) return $updraftplus->log_wp_error($storage, false, true); $bucket_name = untrailingslashit($config['path']); $bucket_path = ""; if (preg_match("#^([^/]+)/(.*)$#", $bucket_name, $bmatches)) { $bucket_name = $bmatches[1]; $bucket_path = $bmatches[2]."/"; } list($storage, $config, $bucket_exists, $region) = $this->get_bucket_access($storage, $config, $bucket_name, $bucket_path);// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- The value is passed back from get_bucket_access if ($bucket_exists) { $file_info = $this->listfiles($file); if (is_array($file_info)) { foreach ($file_info as $finfo) { if ($finfo['name'] == $file) { $file_size = $finfo['size']; break; } } } if (!isset($file_size)) { $this->log("Error: Failed to download $file. Check your permissions and credentials. Retrieved data: ".serialize($file_info)); $this->log(sprintf(__('Error: Failed to download %s.', 'updraftplus'), $file).' '.__('Check your permissions and credentials.', 'updraftplus'), 'error'); return false; } return $updraftplus->chunked_download($file, $this, $file_size, true, $storage, $this->download_chunk_size); /* // The code before we switched to chunked downloads. Unfortunately the version of the AWS SDK we have to use for PHP 5.3 compatibility doesn't have callbacks, which makes it possible for multiple downloaders to start at once and over-write each-other. $whoweare = $config['whoweare']; $fullpath = $updraftplus->backups_dir_location().'/'.$file; if (!$storage->getObject($bucket_name, $bucket_path.$file, $fullpath, true)) { $updraftplus->log("$whoweare Error: Failed to download $file. Check your permissions and credentials."); $updraftplus->log(sprintf(__('%s Error: Failed to download %s.', 'updraftplus'),$whoweare, $file).' '.__(' Check your permissions and credentials.','updraftplus'), 'error'); return false; } */ } else { $this->log("Error: Failed to access bucket $bucket_name. Check your permissions and credentials."); $this->log(sprintf(__('Error: Failed to access bucket %s.', 'updraftplus'), $bucket_name).' '.__('Check your permissions and credentials.', 'updraftplus'), 'error'); return false; } return true; } public function chunked_download($file, $headers, $storage, $fh) { $resume = false; $config = $this->get_config(); $bucket_name = untrailingslashit($config['path']); $bucket_path = ""; if (preg_match("#^([^/]+)/(.*)$#", $bucket_name, $bmatches)) { $bucket_name = $bmatches[1]; $bucket_path = $bmatches[2]."/"; } if (is_array($headers) && !empty($headers['Range']) && preg_match('/bytes=(\d+)-(\d+)$/', $headers['Range'], $matches)) { $resume = $headers['Range']; } if (!$storage->getObject($bucket_name, $bucket_path.$file, $fh, $resume)) { $this->log("Error: Failed to download $file. Check your permissions and credentials."); $this->log(sprintf(__('Error: Failed to download %s.', 'updraftplus'), $file).' '.__('Check your permissions and credentials.', 'updraftplus'), 'error'); return false; } // This instructs the caller to look at the file pointer's position (i.e. ftell($fh)) to work out how many bytes were written. return true; } /** * Get the pre configuration template */ public function get_pre_configuration_template() { ?> <tr class="{{get_template_css_classes false}} S3_pre_config_container"> <td colspan="2"> <img src="{{storage_image_url}}" alt="Amazon Web Services"><br> {{{xmlwriter_existence_label}}} {{{simplexmlelement_existence_label}}} {{{curl_existence_label}}} <br> <p> {{{console_url}}} {{{ssl_certificates_errors_link_text}}} {{{faqs}}} </p> </td> </tr> <?php } /** * Get pre configuration template engine for remote method which is S3 Compatible * DEVELOPER NOTE: Please don't use/call this method anymore as it was used by Amazon S3 and S3-Compatible (Generic) storage, and it's consider to be removed in future versions. Once Amazon S3 and S3-Compatible templates are CSP-compliant, this should be removed and should be placed in the class child instead of the base class. * * @param String $key Remote storage method key which is unique * @param String $whoweare_short Remote storage method short name which is prefix of field label generally * @param String $whoweare_long Remote storage method long name which is generally used in instructions * @param String $console_descrip Remote storage method console description. It is used console link text like "from your %s console" * @param String $console_url Remote storage method console url. It is used for get credential instruction * @param String $opening_html HTML to appear first inside the container */ protected function get_pre_configuration_template_engine($key, $whoweare_short, $whoweare_long, $console_descrip, $console_url, $opening_html = '') { $classes = $this->get_css_classes(false); ?> <tr class="<?php echo esc_attr($classes . ' ' . $whoweare_short . '_pre_config_container');?>"> <td colspan="2"> <?php echo wp_kses_post($opening_html).'<br>'; ?> <?php global $updraftplus_admin; $use_s3_class = $this->indicate_s3_class(); if ('UpdraftPlus_S3_Compat' == $use_s3_class && !class_exists('XMLWriter')) { $updraftplus_admin->show_double_warning('<strong>'.__('Warning', 'updraftplus').':</strong> '. sprintf(__("Your web server's PHP installation does not included a required module (%s).", 'updraftplus'), 'XMLWriter').' '.__("Please contact your web hosting provider's support and ask for them to enable it.", 'updraftplus')); } if (!class_exists('SimpleXMLElement')) { $updraftplus_admin->show_double_warning('<strong>'.__('Warning', 'updraftplus').':</strong> '.sprintf(__("Your web server's PHP installation does not included a required module (%s).", 'updraftplus'), 'SimpleXMLElement').' '.__("Please contact your web hosting provider's support.", 'updraftplus').' '.sprintf(__("UpdraftPlus's %s module <strong>requires</strong> %s.", 'updraftplus'), $whoweare_long, 'SimpleXMLElement').' '.__('Please do not file any support requests; there is no alternative.', 'updraftplus'), $key); } $updraftplus_admin->curl_check($whoweare_long, true, $key); ?> <br> <p> <?php if ($console_url) { $a_tag_html = array('a' => array('href' => array())); echo wp_kses(sprintf(__('Get your access key and secret key from your <a href="%s">%s console</a>, then pick a (globally unique - all %s users) bucket name (letters and numbers) (and optionally a path) to use for storage.', 'updraftplus'), $console_url, $console_descrip, $whoweare_long), $a_tag_html).' '.esc_html__('This bucket will be created for you if it does not already exist.', 'updraftplus'); } ?> <a href="<?php echo esc_url(apply_filters("updraftplus_com_link", "https://updraftplus.com/faqs/i-get-ssl-certificate-errors-when-backing-up-andor-restoring/"));?>" target="_blank"><?php esc_html_e('If you see errors about SSL certificates, then please go here for help.', 'updraftplus');?></a> <a href="<?php echo esc_url(apply_filters("updraftplus_com_link", "https://updraftplus.com/faq-category/amazon-s3/"));?>" target="_blank"><?php if ('s3' == $key) echo esc_html(sprintf(__('Other %s FAQs.', 'updraftplus'), 'S3'));?></a> </p> </td> </tr> <?php } /** * Get the configuration template * * @return String - the template, ready for substitutions to be carried out */ public function get_configuration_template() { // White: https://d36cz9buwru1tt.cloudfront.net/Powered-by-Amazon-Web-Services.jpg // Black: https://awsmedia.s3.amazonaws.com/AWS_logo_poweredby_black_127px.png ob_start(); ?> {{#ifeq "s3" method_id}} {{#> s3_additional_configuration_top}} {{/s3_additional_configuration_top}} {{else}} {{#> s3generic_additional_configuration_top}} {{/s3generic_additional_configuration_top}} {{/ifeq}} <tr class="{{get_template_css_classes true}}"> <th>{{input_access_key_label}}:</th> <td><input class="updraft_input--wide udc-wd-600" data-updraft_settings_test="accesskey" type="text" autocomplete="off" id="{{get_template_input_attribute_value "id" "accesskey"}}" name="{{get_template_input_attribute_value "name" "accesskey"}}" value="{{accesskey}}" /></td> </tr> <tr class="{{get_template_css_classes true}}"> <th>{{input_secret_key_label}}:</th> <td><input class="updraft_input--wide udc-wd-600" data-updraft_settings_test="secretkey" type="{{input_secret_key_type}}" autocomplete="off" id="{{get_template_input_attribute_value "id" "secretkey"}}" name="{{get_template_input_attribute_value "name" "secretkey"}}" value="{{secretkey}}" /></td> </tr> <tr class="{{get_template_css_classes true}}"> <th>{{input_location_label}}:</th> <td>{{method_id}}://<input class="updraft_input--wide udc-wd-600" data-updraft_settings_test="path" title="{{input_location_title}}" type="text" id="{{get_template_input_attribute_value "id" "path"}}" name="{{get_template_input_attribute_value "name" "path"}}" value="{{path}}" /></td> </tr> {{#ifeq "s3" method_id}} {{#> s3_additional_configuration_bottom}} {{/s3_additional_configuration_bottom}} {{else}} {{#> s3generic_additional_configuration_bottom}} {{/s3generic_additional_configuration_bottom}} {{/ifeq}} {{{get_template_test_button_html method_display_name}}} <?php return ob_get_clean(); } /** * Get partial templates associated to the corresponding backup module (remote storage object) * * @return Array an associative array keyed by names of the partial template */ public function get_partial_templates() { return wp_parse_args(apply_filters('updraft_'.$this->get_id().'_partial_templates', array()), parent::get_partial_templates()); } /** * Modifies handerbar template options * * @param array $opts * @return Array - Modified handerbar template options */ public function transform_options_for_template($opts) { return apply_filters('updraftplus_options_s3_options', $opts); } /** * Get configuration template engine for remote method which is S3 Compatible * DEVELOPER NOTE: Please don't use/call this method anymore as it was used by Amazon S3 and S3-Compatible (Generic) storage, and it's consider to be removed in future versions. Once Amazon S3 and S3-Compatible templates are CSP-compliant, this should be removed and should be placed in the class child instead of the base class. * * @param String $key Remote storage method key which is unique * @param String $whoweare_short Remote storage method short name which is prefix of field label generally * @param String $whoweare_long Remote storage method long name which is generally used in instructions * @param String $console_descrip Remote storage method console description. It is used console link text like "from your %s console" * @param String $console_url Remote storage method console url. It is used for get credential instruction * @param String $img_html Image html tag * * @return String $template_str handlebars template string */ public function get_configuration_template_engine($key, $whoweare_short, $whoweare_long, $console_descrip, $console_url, $img_html = '') {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $whoweare_long, $console_descrip, $console_url, $img_html unused global $updraftplus; ob_start(); $classes = $this->get_css_classes(); $template_str = ''; if ('s3' == $key && version_compare(PHP_VERSION, '5.3.3', '>=') && class_exists('UpdraftPlus_Addon_S3_Enhanced')) { ?> <tr class="<?php echo esc_attr($classes);?>"> <td colspan="2"> <?php echo wp_kses_post(apply_filters('updraft_s3_apikeysetting', '<a href="'.$updraftplus->get_url('premium').'" target="_blank"><em>'.__('To create a new IAM sub-user and access key that has access only to this bucket, upgrade to Premium.', 'updraftplus').'</em></a>')); ?> </td> </tr> <?php } ?> <tr class="<?php echo esc_attr($classes);?>"> <th><?php echo esc_html(sprintf(__('%s access key', 'updraftplus'), $whoweare_short));?>:</th> <td><input class="updraft_input--wide" data-updraft_settings_test="accesskey" type="text" autocomplete="off" <?php $this->output_settings_field_name_and_id('accesskey');?> value="{{accesskey}}" /></td> </tr> <tr class="<?php echo esc_attr($classes);?>"> <th><?php echo esc_html(sprintf(__('%s secret key', 'updraftplus'), $whoweare_short));?>:</th> <td><input class="updraft_input--wide" data-updraft_settings_test="secretkey" type="<?php echo esc_attr(apply_filters('updraftplus_admin_secret_field_type', 'password')); ?>" autocomplete="off" <?php $this->output_settings_field_name_and_id('secretkey');?> value="{{secretkey}}" /></td> </tr> <tr class="<?php echo esc_attr($classes);?>"> <th><?php echo esc_html(sprintf(__('%s location', 'updraftplus'), $whoweare_short));?>:</th> <td><?php echo esc_html($key); ?>://<input class="updraft_input--wide" data-updraft_settings_test="path" title="<?php echo esc_attr(__('Enter only a bucket name or a bucket and path.', 'updraftplus').' '.__('Examples: mybucket, mybucket/mypath', 'updraftplus')); ?>" type="text" <?php $this->output_settings_field_name_and_id('path');?> value="{{path}}" /></td> </tr> <?php $template_str .= ob_get_clean(); $template_str .= $this->get_partial_configuration_template_for_endpoint(); $template_str .= apply_filters('updraft_'.$key.'_extra_storage_options_configuration_template', '', $this); $template_str .= $this->get_test_button_html($whoweare_short); return $template_str; } /** * Get handlebar partial template string for endpoint of s3 compatible remote storage method. Other child class can extend it. * DEVELOPER NOTE: Please don't use/call this method anymore as it was used by S3-Compatible (Generic) storage, and it's consider to be removed in future versions. Once Amazon S3-Compatible templates is CSP-compliant, this should be removed and should be placed in the class child instead of the base class. * * @return String - the partial template */ protected function get_partial_configuration_template_for_endpoint() { return ''; } /** * Retrieve a list of template properties by taking all the persistent variables and methods of the parent class and combining them with the ones that are unique to this module, also the necessary HTML element attributes and texts which are also unique only to this backup module * NOTE: Please sanitise all strings that are required to be shown as HTML content on the frontend side (i.e. wp_kses()), or any other technique to prevent XSS attacks that could come via WP hooks * * @return Array an associative array keyed by names that describe themselves as they are */ public function get_template_properties() { global $updraftplus, $updraftplus_admin; $properties = array( 'storage_image_url' => UPDRAFTPLUS_URL .'/images/aws_logo.png', 'console_url' => wp_kses(sprintf(__('Get your access key and secret key from your <a href="%s">%s console</a>, then pick a (globally unique - all %s users) bucket name (letters and numbers) (and optionally a path) to use for storage.', 'updraftplus'), 'https://aws.amazon.com/console/', 'AWS', $updraftplus->backup_methods[$this->get_id()]).' '.__('This bucket will be created for you if it does not already exist.', 'updraftplus'), $this->allowed_html_for_content_sanitisation()), 'xmlwriter_existence_label' => !apply_filters('updraftplus_s3_xmlwriter_exists', 'UpdraftPlus_S3_Compat' != $this->indicate_s3_class() || class_exists('XMLWriter')) ? wp_kses($updraftplus_admin->show_double_warning('<strong>'.__('Warning', 'updraftplus').':</strong> '.sprintf(__("Your web server's PHP installation does not include a required module (%s).", 'updraftplus'), 'XMLWriter').' '.__("Please contact your web hosting provider's support and ask for them to enable it.", 'updraftplus'), $this->get_id(), false), $this->allowed_html_for_content_sanitisation()) : '', 'simplexmlelement_existence_label' => !apply_filters('updraftplus_s3_simplexmlelement_exists', class_exists('SimpleXMLElement')) ? wp_kses($updraftplus_admin->show_double_warning('<strong>'.__('Warning', 'updraftplus').':</strong> '.sprintf(__("Your web server's PHP installation does not include a required module (%s).", 'updraftplus'), 'SimpleXMLElement').' '.__("Please contact your web hosting provider's support.", 'updraftplus').' '.sprintf(__("UpdraftPlus's %s module <strong>requires</strong> %s.", 'updraftplus'), $updraftplus->backup_methods[$this->get_id()], 'SimpleXMLElement').' '.__('Please do not file any support requests; there is no alternative.', 'updraftplus'), $this->get_id(), false), $this->allowed_html_for_content_sanitisation()) : '', 'curl_existence_label' => wp_kses($updraftplus_admin->curl_check($updraftplus->backup_methods[$this->get_id()], true, $this->get_id().' hide-in-udc', false), $this->allowed_html_for_content_sanitisation()), 'ssl_certificates_errors_link_text' => wp_kses('<a href="'.apply_filters("updraftplus_com_link", "https://updraftplus.com/faqs/i-get-ssl-certificate-errors-when-backing-up-andor-restoring/").'" target="_blank">'.__('If you see errors about SSL certificates, then please go here for help.', 'updraftplus').'</a>', $this->allowed_html_for_content_sanitisation()), 'faqs' => wp_kses('<a href="'.apply_filters("updraftplus_com_link", "https://updraftplus.com/faq-category/amazon-s3/").'" target="_blank">'.sprintf(__('Other %s FAQs.', 'updraftplus'), $updraftplus->backup_methods[$this->get_id()]).'</a>', $this->allowed_html_for_content_sanitisation()), 'input_access_key_label' => sprintf(__('%s access key', 'updraftplus'), $updraftplus->backup_methods[$this->get_id()]), 'input_secret_key_label' => sprintf(__('%s secret key', 'updraftplus'), $updraftplus->backup_methods[$this->get_id()]), 'input_secret_key_type' => apply_filters('updraftplus_admin_secret_field_type', 'password'), 'input_location_label' => sprintf(__('%s location', 'updraftplus'), $updraftplus->backup_methods[$this->get_id()]), 'input_location_title' => __('Enter only a bucket name or a bucket and path.', 'updraftplus').' '.__('Examples: mybucket, mybucket/mypath', 'updraftplus'), 'input_test_label' => sprintf(__('Test %s Settings', 'updraftplus'), $updraftplus->backup_methods[$this->get_id()]), ); return wp_parse_args(apply_filters('updraft_'.$this->get_id().'_template_properties', array()), wp_parse_args($properties, $this->get_persistent_variables_and_methods())); } /** * Look at the config, and decide whether or not to call self::use_dns_bucket_name() * * @param Object $storage - S3 Name * @param String $bucket - storage path * @param Array $config - configuration - may not be complete at this stage, so be careful about which properties are used * * @return Boolean - whether or not DNS bucket naming will be used */ protected function maybe_use_dns_bucket_name($storage, $bucket, $config) { if ('s3' === $config['key'] && '' !== $bucket) { if (preg_match("#^([^/]+)/(.*)$#", $bucket, $pmatches)) { $bucket = $pmatches[1]; } else { $bucket = $bucket; } // AWS SSL certificates have wildcards at one level only, i.e. *.s3.amazonaws.com, so cannot be validated if the bucket brings in a further sub-domain level if (false !== strstr($bucket, '.') && $storage->getuseSSL() && $storage->getUseSSLValidation()) return false; if (strlen($bucket) > 63) return false; if (preg_match("/[^a-z0-9\.-]/", $bucket)) return false; // A DNS bucket name cannot contain -. if (false !== strstr($bucket, '-.')) return false; // A DNS bucket name cannot contain .. if (false !== strstr($bucket, '..')) return false; // A DNS bucket name must begin with 0-9a-z if (!preg_match("/^[0-9a-z]/", $bucket)) return false; // A DNS bucket name must end with 0-9 a-z if (!preg_match("/[0-9a-z]$/", $bucket)) return false; return $this->use_dns_bucket_name($storage, $bucket); } return false; } /** * This is not pretty, but is the simplest way to accomplish the task within the pre-existing structure (no need to re-invent the wheel of code with corner-cases debugged over years) * * @param object $storage S3 Name * * @return Boolean - true if the operation was accepted */ public function use_dns_bucket_name($storage) { return is_a($storage, 'UpdraftPlus_S3_Compat') ? true : $storage->useDNSBucketName(true); } /** * Acts as a WordPress options filter * * @param Array $settings - pre-filtered settings * * @return Array filtered settings */ public function options_filter($settings) { if (is_array($settings) && !empty($settings['version']) && !empty($settings['settings'])) { foreach ($settings['settings'] as $instance_id => $instance_settings) { if (!empty($instance_settings['path'])) { $settings['settings'][$instance_id]['path'] = trim($instance_settings['path'], "/ \t\n\r\0\x0B"); } } } return $settings; } /** * This method contains some repeated code. After getting an S3 object, it's time to see if we can access that bucket - either immediately, or via creating it, etc. * * @param Object $storage S3 name * @param Array $config array of config details; if the provider does not have the concept of regions, then the key 'endpoint' is required to be set * @param String $bucket S3 Bucket * @param String $path S3 Path * * @return Array - N.B. May contain updated versions of $storage and $config */ private function get_bucket_access($storage, $config, $bucket, $path) { $bucket_exists = false; $use_ssl = true; $ssl_ca = false; $nossl = $this->got_with['nossl']; // Ignore the 'nossl' setting if the endpoint is DigitalOcean Spaces (https://developers.digitalocean.com/documentation/v2/) if (!empty($config['endpoint']) && preg_match('/\.digitaloceanspaces\.com$/i', $config['endpoint'])) { $nossl = apply_filters('updraftplus_gets3_nossl', false, $config['endpoint'], $this->got_with['nossl']); } if (!$nossl) { $curl_version = function_exists('curl_version') ? curl_version() : array('features' => null); $curl_ssl_supported = ($curl_version['features'] && defined('CURL_VERSION_SSL') && CURL_VERSION_SSL); if ($curl_ssl_supported) { if ($this->got_with['disableverify']) { $ssl_ca = false; $this->log("Disabling verification of SSL certificates"); } else { if ($this->got_with['useservercerts']) { $this->log("Using the server's SSL certificates"); $ssl_ca = 'system'; } else { $ssl_ca = file_exists(UPDRAFTPLUS_DIR.'/includes/cacert.pem') ? UPDRAFTPLUS_DIR.'/includes/cacert.pem' : true; } } } else { $use_ssl = false; $this->log("Curl/SSL is not available. Communications will not be encrypted."); } } else { $use_ssl = false; $this->log("SSL was disabled via the user's preference. Communications will not be encrypted."); } $storage->setSSL($use_ssl); $storage->setSSLAuth(null, null, $ssl_ca); // If using Amazon S3, then always prefer using host-based access $this->maybe_use_dns_bucket_name($storage, $bucket, $config); if ($this->provider_has_regions) { $storage->setExceptions(true); if ('dreamobjects' == $config['key']) { $endpoint = isset($config['endpoint']) ? $config['endpoint'] : ''; $this->set_region($storage, $endpoint); } try { $region = @$storage->getBucketLocation($bucket);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise because of the method. // We want to distinguish between an empty region (null), and an exception or missing bucket (false) if (empty($region) && false !== $region) $region = null; } catch (Exception $e) { $region = false; // This is not ideal, but helps with detection/trapping of other "permanent" conditions // All these codes are likely to indicate things that we want to be logged, rather than suppressing logging until the final method used to get a bucket location $curl_error_codes = array(1, 2, 3, 4, 5, 6, 7, 8, 16, 23, 26, 27, 28, 33, 35, 41, 43, 47, 48, 49, 53, 54, 55, 56, 58, 59, 60, 61, 66, 77, 81, 82, 83, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99); if (UpdraftPlus_Options::get_updraft_option('updraft_debug_mode') || (preg_match('/\[(\d+)\]/', $e->getMessage(), $matches) && in_array($matches[1], $curl_error_codes))) { global $updraftplus; $updraftplus->log("get_bucket_access(): getBucketLocation exception (".get_class($e)."): ".$e->getMessage()); } // On this 'first try', we trap these particular conditions. So, whatever S3 network call it happens on, we'll eventually get it here on the resumption. if (false !== strpos($e->getMessage(), '[RequestTimeTooSkewed]')) { $this->s3_exception = $e; return array($storage, $config, false, false); } // The base Amazon S3 module and child S3-Generic remote storage don't use session token to make a connection to the targeted storage server: we only use session token for Vault storage. // Since we don't provide credentials testing for Vault storage, this means handling session token expiry exception won't happen during credentials testing. So the fact that saved values are used here is fine, since there are no other relevant values in the absence of credentials testing. if (false !== strpos($e->getMessage(), 'The provided token has expired')) { $this->log($e->getMessage().": Requesting new credentials"); $new_config = $this->get_config(true); if (empty($new_config['sessiontoken'])) $new_config['sessiontoken'] = null; if (!empty($new_config['accesskey'])) { $new_storage = $this->getS3( $new_config['accesskey'], $new_config['secretkey'], UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts'), UpdraftPlus_Options::get_updraft_option('updraft_ssl_disableverify'), UpdraftPlus_Options::get_updraft_option('updraft_ssl_nossl'), null, !empty($new_config['server_side_encryption']), $new_config['sessiontoken'] ); if (!is_wp_error($new_storage)) { // Try again try { $region = @$new_storage->getBucketLocation($bucket);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise because of the method. // We want to distinguish between an empty region (null), and an exception or missing bucket (false) if (empty($region) && false !== $region) $region = null; // Worked this time; update the passed-in information $storage = $new_storage; $config = $new_config; } catch (Exception $e) { $region = false; } } } } } $storage->setExceptions(false); } else { $region = false; $this->set_region($storage, $config['endpoint'], $bucket); } // See if we can detect the region (which implies the bucket exists and is ours), or if not create it if (!$this->provider_has_regions || false === $region) { $storage->setExceptions(true); try { if (@$storage->putBucket($bucket, 'private')) {// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise because of the method. $bucket_exists = true; } } catch (Exception $e) { $this->s3_exception = $e; try { if (false !== @$storage->getBucket($bucket, $path, null, 1)) {// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise because of the method. $bucket_exists = true; } } catch (Exception $e) { // We don't put this in a separate catch block which names the exception, since we need to remain compatible with PHP 5.2 if (is_a($storage, 'UpdraftPlus_S3_Compat') && is_a($e, 'Aws\S3\Exception\S3Exception')) { $response = $e->getResponse(); if (is_null($response)) { $this->s3_exception = $e; } else { $xml = new SimpleXMLElement((string) $response->getBody(), LIBXML_NONET); if (!empty($xml->Code) && 'AuthorizationHeaderMalformed' == $xml->Code->__toString() && !empty($xml->Region)) { $this->set_region($storage, $xml->Region->__toString()); $storage->setExceptions(false); if (false !== @$storage->getBucket($bucket, $path, null, 1)) {// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise because of the method. $bucket_exists = true; } } else { $this->s3_exception = $e; } } } else { $this->s3_exception = $e; } } } $storage->setExceptions(false); } else { $bucket_exists = true; } // For a region-less S3 system, we set this to true so that we can carry on trying anyway, since the behaviour of different S3-compatible systems can vary. e.g. DigitalOcean spaces API keys allow you to create a bucket. if (!$this->provider_has_regions) { $bucket_exists = true; } elseif ($bucket_exists) { if ('s3' == $config['key'] || 'updraftvault' == $config['key']) { $this->set_region($storage, $region, $bucket); } elseif (!empty($region)) { // N.B. region non-empty here implies that it's dreamobjects; but in that case, we already called set_region earlier. So, this is now commented (May 2021) // if (!$endpoint) $this->set_region($storage, $endpoint, $bucket); } } return array($storage, $config, $bucket_exists, $region); } /** * Perform a test of user-supplied credentials, and echo the result * * @param Array $posted_settings - settings to test */ public function credentials_test($posted_settings) { if (empty($posted_settings['accesskey'])) { echo esc_html(sprintf(__("Failure: No %s was given.", 'updraftplus'), __('API key', 'updraftplus'))); return; } if (empty($posted_settings['secretkey'])) { echo esc_html(sprintf(__("Failure: No %s was given.", 'updraftplus'), __('API secret', 'updraftplus'))); return; } $key = $posted_settings['accesskey']; $secret = $posted_settings['secretkey']; $path = $posted_settings['path']; $useservercerts = isset($posted_settings['useservercerts']) ? absint($posted_settings['useservercerts']) : 0; $disableverify = isset($posted_settings['disableverify']) ? absint($posted_settings['disableverify']) : 0; $nossl = isset($posted_settings['nossl']) ? absint($posted_settings['nossl']) : 0; $endpoint = isset($posted_settings['endpoint']) ? trim($posted_settings['endpoint']) : ''; $sse = empty($posted_settings['server_side_encryption']) ? false : true; if (preg_match("#^/*([^/]+)/(.*)$#", $path, $bmatches)) { $bucket = $bmatches[1]; $path = trailingslashit($bmatches[2]); } else { $bucket = $path; $path = ""; } if (empty($bucket)) { esc_html_e("Failure: No bucket details were given.", 'updraftplus'); return; } if (!$this->provider_has_regions && '' == $endpoint) { esc_html_e("Failure: No endpoint details were given.", 'updraftplus'); return; } $config = $this->get_config(); if ('' !== $endpoint) $config['endpoint'] = $endpoint; $whoweare = $config['whoweare']; $session_token = empty($config['sessiontoken']) ? null : $config['sessiontoken']; $storage = $this->getS3($key, $secret, $useservercerts, $disableverify, $nossl, null, $sse, $session_token); if (is_wp_error($storage)) { foreach ($storage->get_error_messages() as $msg) { echo esc_html($msg)."\n"; } return; } if (!empty($posted_settings['signature_version'])) $config['signature_version'] = $posted_settings['signature_version']; if (!empty($posted_settings['bucket_access_style']) && 'virtual_host_style' === $posted_settings['bucket_access_style']) { $storage->useDNSBucketName(true, $bucket); } else { $storage->useDNSBucketName(false, $bucket); } list($storage, $config, $bucket_exists, $region) = $this->get_bucket_access($storage, $config, $bucket, $path); $bucket_verb = ''; if ($this->provider_has_regions && $region) { if ('s3' == $config['key']) { $bucket_verb = __('Region', 'updraftplus').": $region: "; } } if (empty($bucket_exists)) { echo esc_html(__('Failure: We could not successfully access or create such a bucket.', 'updraftplus').' '.sprintf(__('Please check your access credentials, and if those are correct then try another bucket name (as another %s user may already have taken your name).', 'updraftplus'), $whoweare)); if (!empty($this->s3_exception)) echo "\n\n".esc_html(sprintf(__('The error reported by %s was:', 'updraftplus'), $whoweare).' '.$this->s3_exception); if ('s3' == $config['key'] && 'AK' != substr($key, 0, 2)) echo "\n\n".esc_html(sprintf(__('The AWS access key looks to be wrong (valid %s access keys begin with "AK")', 'updraftplus'), $whoweare)); } else { $try_file = md5(rand()); $storage->setExceptions(true); try { if (!$storage->putObjectString($try_file, $bucket, $path.$try_file)) { echo esc_html(__('Failure', 'updraftplus').": {$bucket_verb}".__('We successfully accessed the bucket, but the attempt to create a file in it failed.', 'updraftplus')); } else { echo esc_html(__('Success', 'updraftplus').": {$bucket_verb}".__('We accessed the bucket, and were able to create files within it.', 'updraftplus')).' '; $comm_with = ('' !== $endpoint) ? $endpoint : $config['whoweare_long']; if ($storage->getuseSSL()) { echo esc_html(sprintf(__('The communication with %s was encrypted.', 'updraftplus'), $comm_with)); } else { echo esc_html(sprintf(__('The communication with %s was not encrypted.', 'updraftplus'), $comm_with)); } $create_success = true; } } catch (Exception $e) { echo esc_html(__('Failure', 'updraftplus').": {$bucket_verb}".__('We successfully accessed the bucket, but the attempt to create a file in it failed.', 'updraftplus').' '.__('Please check your access credentials.', 'updraftplus').' ('.$e->getMessage().')'); } if (!empty($create_success)) { try { @$storage->deleteObject($bucket, $path.$try_file);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise because of the method. } catch (Exception $e) { echo esc_html(' '.__('Delete failed:', 'updraftplus').' '.$e->getMessage()); } } } } /** * Check whether options have been set up by the user, or not * * @param Array $opts - the potential options * * @return Boolean */ public function options_exist($opts) { if (is_array($opts) && isset($opts['accesskey']) && '' != $opts['accesskey'] && isset($opts['secretkey'])) return true; return false; } } openstack2.php 0000644 00000031116 15231511727 0007334 0 ustar 00 <?php if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed.'); // SDK uses namespacing - requires PHP 5.3 (actually the SDK states its requirements as 5.3.3) // @codingStandardsIgnoreLine use OpenCloud\OpenStack; updraft_try_include_file('methods/openstack-base.php', 'require_once'); class UpdraftPlus_BackupModule_openstack extends UpdraftPlus_BackupModule_openstack_base { public function __construct() { // 4th parameter is a relative (to UPDRAFTPLUS_DIR) logo URL, which should begin with /, should we get approved for use of the OpenStack logo in future (have requested info) parent::__construct('openstack', 'OpenStack', 'OpenStack (Swift)', ''); } /** * Get Openstack service * * @param String $opts THis contains: 'tenant', 'user', 'password', 'authurl', (optional) 'region' * @param Boolean $useservercerts User server certificates * @param String $disablesslverify Check to disable SSL Verify * @return Array */ public function get_openstack_service($opts, $useservercerts = false, $disablesslverify = null) { // 'tenant', 'user', 'password', 'authurl', 'path', (optional) 'region' extract($opts); if (null === $disablesslverify) $disablesslverify = UpdraftPlus_Options::get_updraft_option('updraft_ssl_disableverify'); if (empty($user) || empty($password) || empty($authurl)) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable -- $user, $password and $authurl being extracted in extract() line 29 throw new Exception(__('Authorisation failed (check your credentials)', 'updraftplus')); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Error message to be escaped when caught and printed. } updraft_try_include_file('vendor/autoload.php', 'include_once'); global $updraftplus; $updraftplus->log("OpenStack authentication URL: ".$authurl);// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable -- $authurl being extracted in extract() line 29 $client = new OpenStack($authurl, array(// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable -- $authurl being extracted in extract() line 29 'username' => $user,// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable -- $user being extracted in extract() line 29 'password' => $password,// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable -- $password being extracted in extract() line 29 'tenantName' => $tenant// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable -- $tenant being extracted in extract() line 29 )); $this->client = $client; if ($disablesslverify) { $client->setSslVerification(false); } else { if ($useservercerts) { $client->setConfig(array($client::SSL_CERT_AUTHORITY => false)); } else { $client->setSslVerification(UPDRAFTPLUS_DIR.'/includes/cacert.pem', true, 2); } } $client->authenticate(); if (empty($region)) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable -- The variable is defined below. $catalog = $client->getCatalog(); if (!empty($catalog)) { $items = $catalog->getItems(); if (is_array($items)) { foreach ($items as $item) { $name = $item->getName(); $type = $item->getType(); if ('swift' != $name || 'object-store' != $type) continue; $eps = $item->getEndpoints(); if (!is_array($eps)) continue; foreach ($eps as $ep) { if (is_object($ep) && !empty($ep->region)) { $region = $ep->region; } } } } } } $this->region = $region; return $client->objectStoreService('swift', $region); } /** * This method overrides the parent method and lists the supported features of this remote storage option. * * @return Array - an array of supported features (any features not * mentioned are assumed to not be supported) */ public function get_supported_features() { // This options format is handled via only accessing options via $this->get_options() return array('multi_options', 'config_templates', 'multi_storage', 'conditional_logic'); } /** * Retrieve default options for this remote storage module. * * @return Array - an array of options */ public function get_default_options() { return array( 'user' => '', 'authurl' => '', 'password' => '', 'tenant' => '', 'path' => '', 'region' => '' ); } public function credentials_test($posted_settings) { if (empty($posted_settings['user'])) { echo esc_html(sprintf(__("Failure: No %s was given.", 'updraftplus'), __('username', 'updraftplus'))); return; } if (empty($posted_settings['password'])) { echo esc_html(sprintf(__("Failure: No %s was given.", 'updraftplus'), __('password', 'updraftplus'))); return; } if (empty($posted_settings['tenant'])) { echo esc_html(sprintf(__("Failure: No %s was given.", 'updraftplus'), _x('tenant', '"tenant" is a term used with OpenStack storage - Google for "OpenStack tenant" to get more help on its meaning', 'updraftplus'))); return; } if (empty($posted_settings['authurl'])) { echo esc_html(sprintf(__("Failure: No %s was given.", 'updraftplus'), __('authentication URI', 'updraftplus'))); return; } $opts = array( 'user' => $posted_settings['user'], 'password' => $posted_settings['password'], 'authurl' => $posted_settings['authurl'], 'tenant' => $posted_settings['tenant'], 'region' => empty($posted_settings['region']) ? '' : $posted_settings['region'], ); $this->credentials_test_go($opts, $posted_settings['path'], $posted_settings['useservercerts'], $posted_settings['disableverify']); } /** * Check whether options have been set up by the user, or not * * @param Array $opts - the potential options * * @return Boolean */ public function options_exist($opts) { if (is_array($opts) && $opts['user'] && '' !== $opts['user'] && !empty($opts['authurl'])) return true; return false; } /** * Get the pre configuration template * * @return String - the template */ public function get_pre_configuration_template() { ?> <tr class="{{get_template_css_classes false}} {{method_id}}_pre_config_container"> <td colspan="2"> {{#if storage_image_url}} <img alt="{{storage_long_description}}" src="{{storage_image_url}}"> {{/if}} <br> {{{mb_substr_existence_label}}} {{{curl_existence_label}}} <br> <p>{{openstack_text_description}} <a href="{{faq_link_url}}" target="_blank">{{faq_link_text}}</a></p> </td> </tr> <?php } /** * Get the configuration template * * @return String - the template, ready for substitutions to be carried out */ public function get_configuration_template() { ob_start(); ?> <tr class="{{get_template_css_classes true}}"> <th>{{input_authentication_uri_label}}:</th> <td><input title="{{input_authentication_uri_title}}" data-updraft_settings_test="authurl" type="text" autocomplete="off" class="updraft_input--wide udc-wd-600" id="{{get_template_input_attribute_value "id" "authurl"}}" name="{{get_template_input_attribute_value "name" "authurl"}}" value="{{authurl}}" /> <br> <em>{{input_authentication_uri_title}}</em> </td> </tr> <tr class="{{get_template_css_classes true}}"> <th><a href="{{input_tenant_link_url}}" title="{{input_tenant_link_title}}" target="_blank">{{input_tenant_label}}</a>:</th> <td><input data-updraft_settings_test="tenant" type="text" autocomplete="off" class="updraft_input--wide udc-wd-600" id="{{get_template_input_attribute_value "id" "tenant"}}" name="{{get_template_input_attribute_value "name" "tenant"}}" value="{{tenant}}" /> </td> </tr> <tr class="{{get_template_css_classes true}}"> <th>{{input_region_label}}:</th> <td><input title="{{input_region_title}}" data-updraft_settings_test="region" type="text" autocomplete="off" class="updraft_input--wide udc-wd-600" id="{{get_template_input_attribute_value "id" "region"}}" name="{{get_template_input_attribute_value "name" "region"}}" value="{{region}}" /> <br> <em>{{input_region_title}}</em> </td> </tr> <tr class="{{get_template_css_classes true}}"> <th>{{input_username_label}}:</th> <td><input data-updraft_settings_test="user" type="text" autocomplete="off" class="updraft_input--wide udc-wd-600" id="{{get_template_input_attribute_value "id" "user"}}" name="{{get_template_input_attribute_value "name" "user"}}" value="{{user}}" /> </td> </tr> <tr class="{{get_template_css_classes true}}"> <th>{{input_password_label}}:</th> <td><input data-updraft_settings_test="password" type="{{input_password_type}}" autocomplete="off" class="updraft_input--wide udc-wd-600" id="{{get_template_input_attribute_value "id" "password"}}" name="{{get_template_input_attribute_value "name" "password"}}" value="{{password}}" /> </td> </tr> <tr class="{{get_template_css_classes true}}"> <th>{{input_container_label}}:</th> <td><input data-updraft_settings_test="path" type="text" class="updraft_input--wide udc-wd-600" id="{{get_template_input_attribute_value "id" "path"}}" name="{{get_template_input_attribute_value "name" "path"}}" value="{{path}}" /></td> </tr> {{{get_template_test_button_html "OpenStack (Swift)"}}} <?php return ob_get_clean(); } /** * Retrieve a list of template properties by taking all the persistent variables and methods of the parent class and combining them with the ones that are unique to this module, also the necessary HTML element attributes and texts which are also unique only to this backup module * NOTE: Please sanitise all strings that are required to be shown as HTML content on the frontend side (i.e. wp_kses()), or any other technique to prevent XSS attacks that could come via WP hooks * * @return Array an associative array keyed by names that describe themselves as they are */ public function get_template_properties() { global $updraftplus, $updraftplus_admin; $properties = array( 'storage_image_url' => !empty($this->img_url) ? UPDRAFTPLUS_URL.$this->img_url : '', 'storage_long_description' => $this->long_desc, 'mb_substr_existence_label' => !apply_filters('updraftplus_openstack_mbsubstr_exists', function_exists('mb_substr')) ? wp_kses($updraftplus_admin->show_double_warning('<strong>'.__('Warning', 'updraftplus').':</strong> '.sprintf(__('Your web server\'s PHP installation does not include a required module (%s).', 'updraftplus'), 'mbstring').' '.__('Please contact your web hosting provider\'s support.', 'updraftplus').' '.sprintf(__("UpdraftPlus's %s module <strong>requires</strong> %s.", 'updraftplus'), $this->desc, 'mbstring').' '.__('Please do not file any support requests; there is no alternative.', 'updraftplus'), $this->method, false), $this->allowed_html_for_content_sanitisation()) : '', 'curl_existence_label' => wp_kses($updraftplus_admin->curl_check($this->long_desc, false, $this->method.' hidden-in-updraftcentral', false), $this->allowed_html_for_content_sanitisation()), 'openstack_text_description' => __('Get your access credentials from your OpenStack Swift provider, and then pick a container name to use for storage.', 'updraftplus').' '.__('This container will be created for you if it does not already exist.', 'updraftplus'), 'faq_link_text' => __('Also, you should read this important FAQ.', 'updraftplus'), 'faq_link_url' => wp_kses(apply_filters("updraftplus_com_link", "https://updraftplus.com/faqs/there-appear-to-be-lots-of-extra-files-in-my-rackspace-cloud-files-container/"), array(), array('http', 'https')), 'input_authentication_uri_label' => __('Authentication URI', 'updraftplus'), 'input_authentication_uri_title' => _x('This needs to be a v2 (Keystone) authentication URI; v1 (Swauth) is not supported.', 'Keystone and swauth are technical terms which cannot be translated', 'updraftplus'), 'input_tenant_label' => __('Tenant', 'updraftplus'), 'input_tenant_link_url' => 'https://docs.openstack.org/openstack-ops/content/projects_users.html', 'input_tenant_link_title' => __('Follow this link for more information', 'updraftplus'), 'input_region_label' => __('Region', 'updraftplus'), 'input_region_title' => __('Leave this blank, and a default will be chosen.', 'updraftplus'), 'input_username_label' => __('Username', 'updraftplus'), 'input_password_label' => __('Password', 'updraftplus'), 'input_password_type' => apply_filters('updraftplus_admin_secret_field_type', 'password'), 'input_container_label' => __('Container', 'updraftplus'), 'input_test_label' => sprintf(__('Test %s Settings', 'updraftplus'), $updraftplus->backup_methods[$this->get_id()]), ); return wp_parse_args($properties, $this->get_persistent_variables_and_methods()); } } dropbox.php 0000644 00000130444 15231511727 0006744 0 ustar 00 <?php /** * https://www.dropbox.com/developers/apply?cont=/developers/apps */ if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed.'); // Converted to multi-options (Feb 2017-) and previous options conversion removed: Yes if (!class_exists('UpdraftPlus_BackupModule')) updraft_try_include_file('methods/backup-module.php', 'require_once'); // Fix a potential problem for users who had the short-lived 1.12.35-1.12.38 free versions (see: https://wordpress.org/support/topic/1-12-37-dropbox-auth-broken/page/2/#post-8981457) // Can be removed after a few months $potential_options = UpdraftPlus_Options::get_updraft_option('updraft_dropbox'); if (is_array($potential_options) && isset($potential_options['version']) && isset($potential_options['settings']) && array() === $potential_options['settings']) { // Wipe it, which will force its re-creation in proper format UpdraftPlus_Options::delete_updraft_option('updraft_dropbox'); } class UpdraftPlus_BackupModule_dropbox extends UpdraftPlus_BackupModule { private $current_file_hash; private $current_file_size; private $uploaded_offset; private $upload_tick; /** * This callback is called as upload progress is made * * @param Integer $offset - the byte offset * @param String $uploadid - identifier for the upload in progress * @param Boolean|String $fullpath - optional full path to the file being uploaded */ public function chunked_callback($offset, $uploadid, $fullpath = false) { global $updraftplus; $storage = $this->get_storage(); // Update upload ID $this->jobdata_set('upload_id_'.$this->current_file_hash, $uploadid); $this->jobdata_set('upload_offset_'.$this->current_file_hash, $offset); $time_now = microtime(true); $time_since_last_tick = $time_now - $this->upload_tick; $data_since_last_tick = $offset - $this->uploaded_offset; $this->upload_tick = $time_now; $this->uploaded_offset = $offset; // Here we use job-wide data, because we don't expect wildly different performance for different Dropbox accounts $chunk_size = $updraftplus->jobdata_get('dropbox_chunk_size', 1048576); // Don't go beyond 10MB, or change the chunk size after the last segment if ($chunk_size < 10485760 && $this->current_file_size > 0 && $offset < $this->current_file_size) { $job_run_time = $time_now - $updraftplus->job_time_ms; if ($time_since_last_tick < 10) { $upload_rate = $data_since_last_tick / max($time_since_last_tick, 1); $upload_secs = min(floor($job_run_time), 10); if ($job_run_time < 15) $upload_secs = max(6, $job_run_time*0.6); $new_chunk = (int) max(min($upload_secs * $upload_rate * 0.9, 10485760), 1048576); $new_chunk = $new_chunk - ($new_chunk % 524288); $chunk_size = $new_chunk; $storage->setChunkSize($chunk_size); $updraftplus->jobdata_set('dropbox_chunk_size', $chunk_size); } } if ($this->current_file_size > 0) { $percent = round(100*($offset/$this->current_file_size), 1); $updraftplus->record_uploaded_chunk($percent, "$uploadid, $offset, ".round($chunk_size/1024, 1)." KB", $fullpath); } else { $this->log("Chunked Upload: $offset bytes uploaded"); // This act is done by record_uploaded_chunk, and helps prevent overlapping runs if ($fullpath) touch($fullpath); } } /** * Supported features * * @return Array */ public function get_supported_features() { // This options format is handled via only accessing options via $this->get_options() return array('multi_options', 'config_templates', 'multi_storage', 'conditional_logic', 'manual_authentication'); } /** * Default options * * @return Array */ public function get_default_options() { return array( 'appkey' => '', 'secret' => '', 'folder' => '', 'tk_access_token' => '', ); } /** * Check whether options have been set up by the user, or not * * @param Array $opts - the potential options * * @return Boolean */ public function options_exist($opts) { if (is_array($opts) && !empty($opts['tk_access_token'])) return true; return false; } /** * Acts as a WordPress options filter * * @param Array $dropbox - An array of Dropbox options * @return Array - the returned array can either be the set of updated Dropbox settings or a WordPress error array */ public function options_filter($dropbox) { // Get the current options (and possibly update them to the new format) $opts = UpdraftPlus_Storage_Methods_Interface::update_remote_storage_options_format('dropbox'); if (is_wp_error($opts)) { if ('recursion' !== $opts->get_error_code()) { $msg = "(".$opts->get_error_code()."): ".$opts->get_error_message(); $this->log($msg); error_log("UpdraftPlus: $msg"); } // The saved options had a problem; so, return the new ones return $dropbox; } // If the input is not as expected, then return the current options if (!is_array($dropbox)) return $opts; // Remove instances that no longer exist foreach ($opts['settings'] as $instance_id => $storage_options) { if (!isset($dropbox['settings'][$instance_id])) unset($opts['settings'][$instance_id]); } // Dropbox has a special case where the settings could be empty so we should check for this before if (!empty($dropbox['settings'])) { foreach ($dropbox['settings'] as $instance_id => $storage_options) { if (!empty($opts['settings'][$instance_id]['tk_access_token'])) { $current_app_key = empty($opts['settings'][$instance_id]['appkey']) ? false : $opts['settings'][$instance_id]['appkey']; $new_app_key = empty($storage_options['appkey']) ? false : $storage_options['appkey']; // If a different app key is being used, then wipe the stored token as it cannot belong to the new app if ($current_app_key !== $new_app_key) { unset($opts['settings'][$instance_id]['tk_access_token']); unset($opts['settings'][$instance_id]['ownername']); unset($opts['settings'][$instance_id]['CSRF']); } } // Now loop over the new options, and replace old options with them foreach ($storage_options as $key => $value) { if (null === $value) { unset($opts['settings'][$instance_id][$key]); } else { if (!isset($opts['settings'][$instance_id])) $opts['settings'][$instance_id] = array(); $opts['settings'][$instance_id][$key] = $value; } } if (!empty($opts['settings'][$instance_id]['folder']) && preg_match('#^https?://(www.)dropbox\.com/home/Apps/UpdraftPlus(.Com)?([^/]*)/(.*)$#i', $opts['settings'][$instance_id]['folder'], $matches)) $opts['settings'][$instance_id]['folder'] = $matches[3]; // check if we have the dummy nosave option and remove it so that it doesn't get saved if (isset($opts['settings'][$instance_id]['dummy-nosave'])) unset($opts['settings'][$instance_id]['dummy-nosave']); } } return $opts; } public function backup($backup_array) { global $updraftplus; $opts = $this->get_options(); if (empty($opts['tk_access_token'])) { $this->log('You are not authenticated with Dropbox (1)'); $this->log(__('You are not authenticated with Dropbox', 'updraftplus'), 'error'); return false; } // 28 September 2017: APIv1 is gone. We'll keep the variable to make life easier if there's ever an APIv3. $use_api_ver = 2; if (empty($opts['tk_request_token'])) { $this->log("begin cloud upload (using API version $use_api_ver with OAuth v2 token)"); } else { $this->log("begin cloud upload (using API version $use_api_ver with OAuth v1 token)"); } $chunk_size = $updraftplus->jobdata_get('dropbox_chunk_size', 1048576); try { $dropbox = $this->bootstrap(); if (false === $dropbox) throw new Exception(__('You are not authenticated with Dropbox', 'updraftplus')); $this->log("access gained; setting chunk size to: ".round($chunk_size/1024, 1)." KB"); $dropbox->setChunkSize($chunk_size); } catch (Exception $e) { $this->log('error when trying to gain access: '.$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); $this->log(sprintf(__('error: %s (see log file for more)', 'updraftplus'), $e->getMessage()), 'error'); return false; } $updraft_dir = $updraftplus->backups_dir_location(); foreach ($backup_array as $file) { $available_quota = -1; // If we experience any failures collecting account info, then carry on anyway try { /* Quota information is no longer provided with account information a new call to quotaInfo must be made to get this information. */ $quota_info = $dropbox->quotaInfo(); // Access token expired try to refresh and then call quota info again if ("401" == $quota_info['code']) { $this->log('HTTP code 401 (unauthorized) code returned from Dropbox; attempting to refresh access token'); $dropbox->refreshAccessToken(); $quota_info = $dropbox->quotaInfo(); } if ("200" != $quota_info['code']) { $message = "account/info did not return HTTP 200; returned: ". $quota_info['code']; } elseif (!isset($quota_info['body'])) { $message = "account/info did not return the expected data"; } else { $body = $quota_info['body']; if (isset($body->quota_info)) { $quota_info = $body->quota_info; $total_quota = $quota_info->quota; $normal_quota = $quota_info->normal; $shared_quota = $quota_info->shared; $available_quota = $total_quota - ($normal_quota + $shared_quota); $message = "quota usage: normal=".round($normal_quota/1048576, 1)." MB, shared=".round($shared_quota/1048576, 1)." MB, total=".round($total_quota/1048576, 1)." MB, available=".round($available_quota/1048576, 1)." MB"; } else { $total_quota = max($body->allocation->allocated, 1); $used = $body->used; /* check here to see if the account is a team account and if so use the other used value This will give us their total usage including their individual account and team account */ if (isset($body->allocation->used)) $used = $body->allocation->used; $available_quota = $total_quota - $used; $message = "quota usage: used=".round($used/1048576, 1)." MB, total=".round($total_quota/1048576, 1)." MB, available=".round($available_quota/1048576, 1)." MB"; } } $this->log($message); } catch (Exception $e) { $this->log("exception (".get_class($e).") occurred whilst getting account info: ".$e->getMessage()); // $this->log(sprintf(__("%s error: %s", 'updraftplus'), 'Dropbox', $e->getMessage()).' ('.$e->getCode().')', 'warning', md5($e->getMessage())); } $file_success = 1; $hash = md5($file); $this->current_file_hash = $hash; $filesize = filesize($updraft_dir.'/'.$file); $this->current_file_size = $filesize; // Into KB $filesize = $filesize/1024; $microtime = microtime(true); if ('None' != ($upload_id = $this->jobdata_get('upload_id_'.$hash, 'None', 'updraf_dbid_'.$hash))) { // Resume $offset = $this->jobdata_get('upload_offset_'.$hash, 0, 'updraf_dbof_'.$hash); if ($offset) $this->log("This is a resumption: $offset bytes had already been uploaded"); } else { $offset = 0; $upload_id = 'None'; } // We don't actually abort now - there's no harm in letting it try and then fail if (-1 != $available_quota && $available_quota < ($filesize-$offset)) { $this->log("File upload expected to fail: file data remaining to upload ($file) size is ".($filesize-$offset)." b (overall file size; .".($filesize*1024)." b), whereas available quota is only $available_quota b"); // $this->log(sprintf(__("Account full: your %s account has only %d bytes left, but the file to be uploaded has %d bytes remaining (total size: %d bytes)",'updraftplus'),'Dropbox', $available_quota, $filesize-$offset, $filesize), 'warning'); } $ufile = apply_filters('updraftplus_dropbox_modpath', $file, $this); $this->log("Attempt to upload: $file to: $ufile"); $this->upload_tick = microtime(true); $this->uploaded_offset = $offset; try { $response = $dropbox->chunkedUpload($updraft_dir.'/'.$file, '', $ufile, true, $offset, $upload_id, array($this, 'chunked_callback')); if (empty($response['code']) || "200" != $response['code']) { $this->log('Unexpected HTTP code returned from Dropbox: '.$response['code']." (".serialize($response).")"); if ($response['code'] >= 400) { if (401 == $response['code']) { $this->log('HTTP code 401 returned from Dropbox, refreshing access token'); $dropbox->refreshAccessToken(); } $this->log(sprintf(__('error: failed to upload file to %s (see log file for more)', 'updraftplus'), $file), 'error'); $file_success = 0; } else { $this->log(__('did not return the expected response - check your log file for more details', 'updraftplus'), 'warning'); } } } catch (Exception $e) { $this->log("chunked upload exception (".get_class($e)."): ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); if (preg_match("/Submitted input out of alignment: got \[(\d+)\] expected \[(\d+)\]/i", $e->getMessage(), $matches)) { // Try the indicated offset $we_tried = $matches[1]; $dropbox_wanted = (int) $matches[2]; $this->log("not yet aligned: tried=$we_tried, wanted=$dropbox_wanted; will attempt recovery"); $this->uploaded_offset = $dropbox_wanted; $upload_id = $this->jobdata_get('upload_id_'.$hash, 'None', 'updraf_dbid_'.$hash); try { $dropbox->chunkedUpload($updraft_dir.'/'.$file, '', $ufile, true, $dropbox_wanted, $upload_id, array($this, 'chunked_callback')); } catch (Exception $e) { $msg = $e->getMessage(); if (preg_match('/Upload with upload_id .* already completed/', $msg)) { $this->log('returned an error, but apparently indicating previous success: '.$msg); } else { $this->log($msg.' (line: '.$e->getLine().', file: '.$e->getFile().')'); $this->log(sprintf(__('failed to upload file to %s (see log file for more)', 'updraftplus'), $ufile), 'error'); $file_success = 0; if (strpos($msg, 'select/poll returned error') !== false && $this->upload_tick > 0 && time() - $this->upload_tick > 800) { UpdraftPlus_Job_Scheduler::reschedule(60); $this->log("Select/poll returned after a long time: scheduling a resumption and terminating for now"); UpdraftPlus_Job_Scheduler::record_still_alive(); die; } } } } else { $msg = $e->getMessage(); if (preg_match('/Upload with upload_id .* already completed/', $msg)) { $this->log('returned an error, but apparently indicating previous success: '.$msg); } else { $this->log(sprintf(__('failed to upload file to %s (see log file for more)', 'updraftplus'), $ufile), 'error'); $file_success = 0; if (strpos($msg, 'select/poll returned error') !== false && $this->upload_tick > 0 && time() - $this->upload_tick > 800) { UpdraftPlus_Job_Scheduler::reschedule(60); $this->log("Select/poll returned after a long time: scheduling a resumption and terminating for now"); UpdraftPlus_Job_Scheduler::record_still_alive(); die; } } } } if ($file_success) { $updraftplus->uploaded_file($file); $microtime_elapsed = microtime(true)-$microtime; $speedps = ($microtime_elapsed > 0) ? $filesize/$microtime_elapsed : 0; $speed = sprintf("%.2d", $filesize)." KB in ".sprintf("%.2d", $microtime_elapsed)."s (".sprintf("%.2d", $speedps)." KB/s)"; $this->log("File upload success (".$file."): $speed"); $this->jobdata_delete('upload_id_'.$hash, 'updraf_dbid_'.$hash); $this->jobdata_delete('upload_offset_'.$hash, 'updraf_dbof_'.$hash); } } return null; } /** * This method gets a list of files from the remote storage that match the string passed in and returns an array of backups * * @param String $match a substring to require (tested via strpos() !== false) * @return Array */ public function listfiles($match = 'backup_') { $opts = $this->get_options(); if (empty($opts['tk_access_token'])) return new WP_Error('no_settings', __('No settings were found', 'updraftplus').' (dropbox)'); try { $dropbox = $this->bootstrap(); } catch (Exception $e) { $this->log('access error: '.$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); return new WP_Error('access_error', $e->getMessage()); } $searchpath = '/'.untrailingslashit(apply_filters('updraftplus_dropbox_modpath', '', $this)); try { /* Some users could have a large amount of backups, the max search is 1000 entries we should continue to search until there are no more entries to bring back. */ $cursor = ''; $matches = array(); while (true) { $search = $dropbox->search($match, $searchpath, 1000, $cursor); if (empty($search['code']) || 200 != $search['code']) return new WP_Error('response_error', sprintf(__('%s returned an unexpected HTTP response: %s', 'updraftplus'), 'Dropbox', $search['code']), $search['body']); if (empty($search['body'])) return array(); if (isset($search['body']->matches) && is_array($search['body']->matches)) { $matches = array_merge($matches, $search['body']->matches); } elseif (is_array($search['body'])) { $matches = $search['body']; } else { break; } if (isset($search['body']->has_more) && true == $search['body']->has_more && isset($search['body']->cursor)) { $cursor = $search['body']->cursor; } else { break; } } } catch (Exception $e) { $this->log($e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); // The most likely cause of a search_error is specifying a non-existent path, which should just result in an empty result set. // return new WP_Error('search_error', $e->getMessage()); return array(); } $results = array(); foreach ($matches as $item) { $item = $item->metadata; if (!is_object($item)) continue; if (isset($item->metadata)) $item = $item->metadata; // 2/files/search_v2 has a slightly different output structure compared to 2/files/search model if ((!isset($item->size) || $item->size > 0) && 'folder' != $item->{'.tag'} && !empty($item->path_display) && 0 === strpos($item->path_display, $searchpath)) { $path = substr($item->path_display, strlen($searchpath)); if ('/' == substr($path, 0, 1)) $path = substr($path, 1); // Ones in subfolders are not wanted if (false !== strpos($path, '/')) continue; $result = array('name' => $path); if (!empty($item->size)) $result['size'] = $item->size; $results[] = $result; } } return $results; } /** * Identification of Dropbox app * * @return Array */ private function defaults() { return apply_filters('updraftplus_dropbox_defaults', array('Z3Q3ZmkwbnplNHA0Zzlx', 'bTY0bm9iNmY4eWhjODRt')); } /** * Delete files from the service using the Dropbox API * * @param Array $files - array of filenames to delete * @param Array $data - unused here * @param Array $sizeinfo - unused here * @return Boolean|String - either a boolean true or an error code string */ public function delete($files, $data = null, $sizeinfo = array()) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $data and $sizeinfo unused if (is_string($files)) $files = array($files); $opts = $this->get_options(); if (empty($opts['tk_access_token'])) { $this->log('You are not authenticated with Dropbox (3)'); $this->log(sprintf(__('You are not authenticated with %s (whilst deleting)', 'updraftplus'), 'Dropbox'), 'warning'); return 'authentication_fail'; } try { $dropbox = $this->bootstrap(); } catch (Exception $e) { $this->log($e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); $this->log(sprintf(__('Failed to access %s when deleting (see log file for more)', 'updraftplus'), 'Dropbox'), 'warning'); return 'service_unavailable'; } if (false === $dropbox) return false; $any_failures = false; foreach ($files as $file) { $ufile = apply_filters('updraftplus_dropbox_modpath', $file, $this); $this->log("request deletion: $ufile"); try { $dropbox->delete($ufile); $file_success = 1; } catch (Exception $e) { $this->log($e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); } if (isset($file_success)) { $this->log('deletion succeeded'); } else { $this->log('deletion failed'); $any_failures = true; } } return $any_failures ? 'file_delete_error' : true; } public function download($file) { global $updraftplus; $opts = $this->get_options(); if (empty($opts['tk_access_token'])) { $this->log('You are not authenticated with Dropbox (4)'); $this->log(sprintf(__('You are not authenticated with %s', 'updraftplus'), 'Dropbox'), 'error'); return false; } try { $dropbox = $this->bootstrap(); } catch (Exception $e) { $this->log($e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); $this->log($e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')', 'error'); return false; } if (false === $dropbox) return false; $remote_files = $this->listfiles($file); foreach ($remote_files as $file_info) { if ($file_info['name'] == $file) { return $updraftplus->chunked_download($file, $this, $file_info['size'], apply_filters('updraftplus_dropbox_downloads_manually_break_up', false), null, 2*1048576); } } $this->log("$file: file not found in listing of remote directory"); return false; } /** * Callback used by by chunked downloading API * * @param String $file - the file (basename) to be downloaded * @param Array $headers - supplied headers * @param Mixed $data - pass-back from our call to the API (which we don't use) * @param resource $fh - the local file handle * * @return String - the data downloaded */ public function chunked_download($file, $headers, $data, $fh) {// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Unused parameter is present because the caller from UpdraftPlus class uses 4 arguments. $opts = $this->get_options();// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- filter use $storage = $this->get_storage(); $try_the_other_one = false;// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- filter use $ufile = apply_filters('updraftplus_dropbox_modpath', $file, $this); $options = array(); if (!empty($headers)) $options['headers'] = $headers; try { $get = $storage->download($ufile, $fh, $options); } catch (Exception $e) { $this->log($e); $this->log($e->getMessage(), 'error'); $get = false; } return $get; } /** * Retrieve a list of template properties by taking all the persistent variables and methods of the parent class and combining them with the ones that are unique to this module, also the necessary HTML element attributes and texts which are also unique only to this backup module * NOTE: Please sanitise all strings that are required to be shown as HTML content on the frontend side (i.e. wp_kses()) * * @return Array an associative array keyed by names that describe themselves as they are */ public function get_template_properties() { global $updraftplus, $updraftplus_admin; $partial_templates = $this->get_partial_templates(); $properties = array( 'storage_image_url' => UPDRAFTPLUS_URL.'/images/dropbox-logo.png', 'storage_image_description' => sprintf(__('%s logo', 'updraftplus'), 'Dropbox'), 'curl_existence_label' => wp_kses($updraftplus_admin->curl_check($updraftplus->backup_methods[$this->get_id()], true, $this->get_id().' hidden-in-updraftcentral', false), $this->allowed_html_for_content_sanitisation()), 'app_authorisation_policy_label' => wp_kses(sprintf(__('Please read %s for use of our %s authorization app (none of your backup data is sent to us).', 'updraftplus'), '<a target="_blank" href="https://updraftplus.com/faqs/what-is-your-privacy-policy-for-the-use-of-your-dropbox-app/">'.__('this privacy policy', 'updraftplus').'</a>', 'Dropbox'), $this->allowed_html_for_content_sanitisation()), 'sub_folders_instruction_label1' => __('Need to use sub-folders?', 'updraftplus'), 'sub_folders_instruction_label2' => sprintf(__('Backups are saved in %s.', 'updraftplus'), 'apps/UpdraftPlus'), 'sub_folders_instruction_label3' => wp_kses(sprintf(__('If you backup several sites into the same Dropbox and want to organize with sub-folders, then %scheck out Premium%s', 'updraftplus'), '<a href="'.apply_filters("updraftplus_com_link", "https://updraftplus.com/shop/").'" target="_blank">', '</a>'), $this->allowed_html_for_content_sanitisation()), 'input_authenticate_with_label' => sprintf(__('Authenticate with %s', 'updraftplus'), __('Dropbox', 'updraftplus')), 'already_authenticated_label' => __('(You are already authenticated).', 'updraftplus'), 'authentication_link_text' => wp_kses(sprintf(__("<strong>After</strong> you have saved your settings (by clicking 'Save Changes' below), then come back here and follow this link to complete authentication with %s.", 'updraftplus'), $updraftplus->backup_methods[$this->get_id()]), $this->allowed_html_for_content_sanitisation()), 'deauthentication_link_text' => sprintf(__("Follow this link to remove these settings for %s.", 'updraftplus'), $updraftplus->backup_methods[$this->get_id()]), 'authentication_label' => __('Ensure you are logged into the correct account before continuing.', 'updraftplus'), 'authorised_redirect_uri_label' => __('You must add the following as the authorised redirect URI in your Dropbox console (under "API Settings") when asked', 'updraftplus'), 'input_app_key_label' => __('Your Dropbox App Key', 'updraftplus'), 'input_app_secret_label' => __('Your Dropbox App Secret', 'updraftplus'), 'partial_templates_contain_input_element' => isset($partial_templates['dropbox_additional_configuration_top']) && preg_match('/<input(?:>|[^>]+>)/i', $partial_templates['dropbox_additional_configuration_top']), 'deauthentication_nonce' => wp_create_nonce($this->get_id().'_deauth_nonce'), ); return wp_parse_args(apply_filters('updraft_'.$this->get_id().'_template_properties', array()), wp_parse_args($properties, $this->get_persistent_variables_and_methods())); } /** * Get the pre configuration template * * @return String - the template */ public function get_pre_configuration_template() { ?> <tr class="{{get_template_css_classes false}} {{method_id}}_pre_config_container"> <td colspan="2"> <img alt="{{storage_image_description}}" src="{{storage_image_url}}"> <br> <p> {{{curl_existence_label}}} </p> <p> {{{app_authorisation_policy_label}}} </p> </td> </tr> <?php } /** * Get remote storage partial templates, the partial template is recognised by its name. To find out a name of partial template, look for the partial call syntax in the template, it's enclosed by double curly braces (i.e. {{> partial_template_name }}) * * @return Array an associative array keyed by name of the partial templates */ public function get_partial_templates() { $partial_templates = array(); ob_start(); ?> <tr class="{{get_template_css_classes true}}"> <td></td> <td><strong>{{sub_folders_instruction_label1}}</strong> {{sub_folders_instruction_label2}} {{{sub_folders_instruction_label3}}}</td> </tr> <?php $partial_templates['dropbox_additional_configuration_top'] = ob_get_clean(); return wp_parse_args(apply_filters('updraft_'.$this->get_id().'_partial_templates', $partial_templates), parent::get_partial_templates()); } /** * Get the configuration template * * @return String - the template, ready for substitutions to be carried out */ public function get_configuration_template() { ob_start(); ?> {{#> dropbox_additional_configuration_top}} {{/dropbox_additional_configuration_top}} <tr class="{{get_template_css_classes true}}"> <th>{{input_authenticate_with_label}}:</th> <td> {{#if is_authenticated}} <p> <strong>{{already_authenticated_label}}</strong> <a class="updraft_deauthlink" href="{{admin_page_url}}?action=updraftmethod-{{method_id}}-auth&page=updraftplus&updraftplus_{{method_id}}auth=deauth&nonce={{deauthentication_nonce}}&updraftplus_instance={{instance_id}}" data-instance_id="{{instance_id}}" data-remote_method="{{method_id}}">{{deauthentication_link_text}}</a> </p> {{/if}} {{#if ownername_sentence}} <br/> {{ownername_sentence}} {{/if}} <p> {{authentication_label}} <a class="updraft_authlink" href="{{admin_page_url}}?&action=updraftmethod-{{method_id}}-auth&page=updraftplus&updraftplus_{{method_id}}auth=doit&nonce={{storage_auth_nonce}}&updraftplus_instance={{instance_id}}" data-instance_id="{{instance_id}}" data-remote_method="{{method_id}}">{{{authentication_link_text}}}</a> </p> </td> </tr> {{!-- Legacy: only show this next setting to old users who had a setting stored --}} {{#if old_user_settings}} <tr class="{{get_template_css_classes true}}"> <th></th> <td> <p>{{authorised_redirect_uri_label}}: <kbd>{{admin_page_url}}?page=updraftplus&action=updraftmethod-dropbox-auth</kbd></p> </td> </tr> <tr class="{{get_template_css_classes true}}"> <th>{{input_app_key_label}}:</th> <td><input type="text" autocomplete="off" style="width:332px" id="{{get_template_input_attribute_value "id" "appkey"}}" name="{{get_template_input_attribute_value "name" "appkey"}}" value="{{appkey}}" /></td> </tr> <tr class="{{get_template_css_classes true}}"> <th>{{input_app_secret_label}}:</th> <td><input type="text" style="width:332px" id="{{get_template_input_attribute_value "id" "secret"}}" name="{{get_template_input_attribute_value "name" "secret"}}" value="{{secret}}" /></td> </tr> {{else}} {{#unless partial_templates_contain_input_element}} {{!-- We need to make sure that it is not the case that the module has no settings whatsoever - this can result in the module being effectively invisible. --}} <input type="hidden" id="{{get_template_input_attribute_value "id" "dummy-nosave"}}" name="{{get_template_input_attribute_value "name" "dummy-nosave"}}" value="0"> {{/unless}} {{/if}} <?php return ob_get_clean(); } /** * Generates ownername with email * * @param array $opts * @return String - Ownername with email */ private function generate_ownername_with_email($opts) { $ownername_with_email = ''; if (!empty($opts['ownername'])) { $ownername_with_email = $opts['ownername']; } if (!empty($opts['email'])) { if (!empty($ownername_with_email)) { $ownername_with_email = $ownername_with_email.' ('.$opts['email'].')'; } else { $ownername_with_email = $opts['email']; } } return $ownername_with_email; } /** * Modifies handerbar template options * * @param array $opts * @return Array - Modified handerbar template options */ public function transform_options_for_template($opts) { if (!empty($opts['tk_access_token'])) { $opts['ownername'] = empty($opts['ownername']) ? '' : $opts['ownername']; $opts['email'] = empty($opts['email']) ? '' : $opts['email']; $ownername_with_email = $this->generate_ownername_with_email($opts); if ($ownername_with_email) { $opts['ownername_sentence'] = sprintf(__("Account holder's name: %s.", 'updraftplus'), $ownername_with_email).' '; } $opts['is_authenticated'] = true; } $opts['old_user_settings'] = (!empty($opts['appkey']) || (defined('UPDRAFTPLUS_CUSTOM_DROPBOX_APP') && UPDRAFTPLUS_CUSTOM_DROPBOX_APP)); if ($opts['old_user_settings']) { $opts['appkey'] = empty($opts['appkey']) ? '' : $opts['appkey']; $opts['secret'] = empty($opts['secret']) ? '' : $opts['secret']; } $opts = apply_filters("updraftplus_options_dropbox_options", $opts); return $opts; } /** * Gives settings keys which values should not passed to handlebarsjs context. * The settings stored in UD in the database sometimes also include internal information that it would be best not to send to the front-end (so that it can't be stolen by a man-in-the-middle attacker) * * @return Array - Settings array keys which should be filtered */ public function filter_frontend_settings_keys() { return array( 'CSRF', 'code', 'ownername', 'tk_access_token', ); } /** * Over-rides the parent to allow this method to output extra information about using the correct account for OAuth authentication * * @return [boolean] - return false so that no extra information is output */ public function output_account_warning() { return true; } /** * Handles various URL actions, as indicated by the updraftplus_dropboxauth URL parameter * * @return null */ public function action_auth() { if (isset($_GET['updraftplus_dropboxauth'])) { if ('doit' == $_GET['updraftplus_dropboxauth']) { $this->action_authenticate_storage(); return; } elseif ('deauth' == $_GET['updraftplus_dropboxauth']) { $this->action_deauthenticate_storage(); return; } } elseif (isset($_REQUEST['state'])) { if ('POST' == $_SERVER['REQUEST_METHOD']) { $raw_state = urldecode($_POST['state']); if (isset($_POST['code'])) $raw_code = urldecode($_POST['code']); } else { $raw_state = $_GET['state']; if (isset($_GET['code'])) $raw_code = $_GET['code']; } if (!empty($raw_code)) $this->do_complete_authentication($raw_state, $raw_code); } try { $this->auth_request(); } catch (Exception $e) { $this->log(sprintf(__("%s error: %s", 'updraftplus'), sprintf(__("%s authentication", 'updraftplus'), 'Dropbox'), $e->getMessage()), 'error'); } } /** * This function will complete the oAuth flow, if return_instead_of_echo is true then add the action to display the authed admin notice, otherwise echo this notice to page. * * @param string $raw_state - the state * @param string $raw_code - the oauth code * @param boolean $return_instead_of_echo - a boolean to indicate if we should return the result or echo it * * @return void|string - returns the authentication message if return_instead_of_echo is true */ public function do_complete_authentication($raw_state, $raw_code, $return_instead_of_echo = false) { // Get the CSRF from setting and check it matches the one returned if it does no CSRF attack has happened $opts = $this->get_options(); $csrf = $opts['CSRF']; $state = stripslashes($raw_state); // Check the state to see if an instance_id has been attached and if it has then extract the state $parts = explode(':', $state); $state = $parts[0]; if (strcmp($csrf, $state) == 0) { $opts['CSRF'] = ''; if (isset($raw_code)) { // set code so it can be accessed in the next authentication step $opts['code'] = stripslashes($raw_code); // remove our flag so we know this authentication is complete if (isset($opts['auth_in_progress'])) unset($opts['auth_in_progress']); $this->set_options($opts, true); $auth_result = $this->auth_token($return_instead_of_echo); if ($return_instead_of_echo) return $auth_result; } } else { error_log("UpdraftPlus: CSRF comparison failure: $csrf != $state"); } } /** * This method will reset any saved options and start the bootstrap process for an authentication * * @param String $instance_id - the instance id of the settings we want to authenticate */ public function do_authenticate_storage($instance_id) { try { // Clear out the existing credentials $opts = $this->get_options(); $opts['tk_access_token'] = ''; unset($opts['tk_request_token']); $opts['ownername'] = ''; // Set a flag so we know this authentication is in progress $opts['auth_in_progress'] = true; $this->set_options($opts, true); $this->set_instance_id($instance_id); $this->bootstrap(false); } catch (Exception $e) { $this->log(sprintf(__("%s error: %s", 'updraftplus'), sprintf(__("%s authentication", 'updraftplus'), 'Dropbox'), $e->getMessage()), 'error'); } } /** * This method will start the bootstrap process for a de-authentication * * @param String $instance_id - the instance id of the settings we want to de-authenticate */ public function do_deauthenticate_storage($instance_id) { try { $this->set_instance_id($instance_id); $this->bootstrap(true); } catch (Exception $e) { $this->log(sprintf(__("%s error: %s", 'updraftplus'), sprintf(__("%s de-authentication", 'updraftplus'), 'Dropbox'), $e->getMessage()), 'error'); } } /** * This method will setup the authenticated admin warning, it can either return this or echo it * * @param boolean $return_instead_of_echo - a boolean to indicate if we should return the result or echo it * * @return void|string - returns the authentication message if return_instead_of_echo is true */ public function show_authed_admin_warning($return_instead_of_echo) { global $updraftplus_admin; $dropbox = $this->bootstrap(); if (false === $dropbox) return false; try { $account_info = $dropbox->accountInfo(); } catch (Exception $e) { $accountinfo_err = sprintf(__("%s error: %s", 'updraftplus'), 'Dropbox', $e->getMessage()).' ('.$e->getCode().')'; } $message = "<strong>".__('Success:', 'updraftplus').'</strong> '.sprintf(__('you have authenticated your %s account', 'updraftplus'), 'Dropbox'); // We log, because otherwise people get confused by the most recent log message of 'Parameter not found: oauth_token' and raise support requests $this->log(__('Success:', 'updraftplus').' '.sprintf(__('you have authenticated your %s account', 'updraftplus'), 'Dropbox')); if (empty($account_info['code']) || "200" != $account_info['code']) { $message .= " (".__('though part of the returned information was not as expected - whether this indicates a real problem cannot be determined', 'updraftplus').") ". $account_info['code']; if (!empty($accountinfo_err)) $message .= "<br>".htmlspecialchars($accountinfo_err); } else { $body = $account_info['body']; $name = ''; $email = ''; if (isset($body->display_name)) { $name = $body->display_name; } else { $name = $body->name->display_name; } if (isset($body->email)) { $email = $body->email; } $opts = $this->get_options(); $opts['ownername'] = $name; $opts['email'] = $email; $ownername_with_email = $this->generate_ownername_with_email($opts); $message .= ". <br>".sprintf(__('Your %s account name: %s', 'updraftplus'), 'Dropbox', htmlspecialchars($ownername_with_email)); $this->set_options($opts, true); try { /** * Quota information is no longer provided with account information a new call to qoutaInfo must be made to get this information. The timeout is because we've seen cases where it returned after 180 seconds (apparently a faulty outgoing proxy), and we may as well wait as cause an error leading to user confusion. */ $quota_info = $dropbox->quotaInfo(array('timeout' => 190)); if (empty($quota_info['code']) || "200" != $quota_info['code']) { $message .= " (".__('though part of the returned information was not as expected - whether this indicates a real problem cannot be determined', 'updraftplus').")". $quota_info['code']; if (!empty($accountinfo_err)) $message .= "<br>".htmlspecialchars($accountinfo_err); } else { $body = $quota_info['body']; if (isset($body->quota_info)) { $quota_info = $body->quota_info; $total_quota = max($quota_info->quota, 1); $normal_quota = $quota_info->normal; $shared_quota = $quota_info->shared; $available_quota =$total_quota - ($normal_quota + $shared_quota); $used_perc = round(($normal_quota + $shared_quota)*100/$total_quota, 1); $message .= ' <br>'.sprintf(__('Your %s quota usage: %s %% used, %s available', 'updraftplus'), 'Dropbox', $used_perc, round($available_quota/1048576, 1).' MB'); } else { $total_quota = max($body->allocation->allocated, 1); $used = $body->used; /* check here to see if the account is a team account and if so use the other used value This will give us their total usage including their individual account and team account */ if (isset($body->allocation->used)) $used = $body->allocation->used; $available_quota =$total_quota - $used; $used_perc = round($used*100/$total_quota, 1); $message .= ' <br>'.sprintf(__('Your %s quota usage: %s %% used, %s available', 'updraftplus'), 'Dropbox', $used_perc, round($available_quota/1048576, 1).' MB'); } } } catch (Exception $e) { // Catch } } if ($return_instead_of_echo) { return "<div class='updraftmessage updated'><p>{$message}</p></div>"; } else { $updraftplus_admin->show_admin_warning($message); } } /** * Bootstrap and check token, can also return the authentication method if return_instead_of_echo is true * * @param boolean $return_instead_of_echo - a boolean to indicate if we should return the result or echo it * * @return void|string - returns the authentication message if return_instead_of_echo is true */ public function auth_token($return_instead_of_echo) { $this->bootstrap(); $opts = $this->get_options(); if (!empty($opts['tk_access_token'])) { if ($return_instead_of_echo) { return $this->show_authed_admin_warning($return_instead_of_echo); } else { add_action('all_admin_notices', array($this, 'show_authed_admin_warning')); } } } /** * Acquire single-use authorization code */ public function auth_request() { $this->bootstrap(); } /** * This basically reproduces the relevant bits of bootstrap.php from the SDK * * @param Boolean $deauthenticate indicates if we should bootstrap for a deauth or auth request * @return object */ public function bootstrap($deauthenticate = false) { $storage = $this->get_storage(); if (!empty($storage) && !is_wp_error($storage)) return $storage; // Dropbox APIv1 is dead, but we'll keep the variable in case v3 is ever announced $dropbox_api = 'Dropbox2'; updraft_try_include_file('includes/'.$dropbox_api.'/API.php', 'include_once'); updraft_try_include_file('includes/'.$dropbox_api.'/Exception.php', 'include_once'); updraft_try_include_file('includes/'.$dropbox_api.'/OAuth/Consumer/ConsumerAbstract.php', 'include_once'); updraft_try_include_file('includes/'.$dropbox_api.'/OAuth/Storage/StorageInterface.php', 'include_once'); updraft_try_include_file('includes/'.$dropbox_api.'/OAuth/Storage/Encrypter.php', 'include_once'); updraft_try_include_file('includes/'.$dropbox_api.'/OAuth/Storage/WordPress.php', 'include_once'); updraft_try_include_file('includes/'.$dropbox_api.'/OAuth/Consumer/Curl.php', 'include_once'); // updraft_try_include_file('includes/'.$dropbox_api.'/OAuth/Consumer/WordPress.php', 'require_once'); $opts = $this->get_options(); $key = empty($opts['secret']) ? '' : $opts['secret']; $sec = empty($opts['appkey']) ? '' : $opts['appkey']; $oauth2_id = defined('UPDRAFTPLUS_DROPBOX_CLIENT_ID') ? UPDRAFTPLUS_DROPBOX_CLIENT_ID : base64_decode('dzQxM3o0cWhqejY1Nm5l'); // Set the callback URL $callbackhome = UpdraftPlus_Options::admin_page_url().'?page=updraftplus&action=updraftmethod-dropbox-auth'; $callback = defined('UPDRAFTPLUS_DROPBOX_AUTH_RETURN_URL') ? UPDRAFTPLUS_DROPBOX_AUTH_RETURN_URL : 'https://auth.updraftplus.com/auth/dropbox/'; if (defined('UPDRAFTPLUS_CUSTOM_DROPBOX_APP') && UPDRAFTPLUS_CUSTOM_DROPBOX_APP) $callback = $callbackhome; $instance_id = $this->get_instance_id(); // Instantiate the Encryptor and storage objects $encrypter = new Dropbox_Encrypter('ThisOneDoesNotMatterBeyondLength'); // Instantiate the storage $dropbox_storage = new Dropbox_WordPress($encrypter, "tk_", 'updraft_dropbox', $this); // WordPress consumer does not yet work // $oauth = new Dropbox_ConsumerWordPress($sec, $key, $dropbox_storage, $callback); // Get the DropBox API access details list($d2, $d1) = $this->defaults(); if (empty($sec)) { $sec = base64_decode($d1); } if (empty($key)) { $key = base64_decode($d2); } $root = 'sandbox'; if ('dropbox:' == substr($sec, 0, 8)) { $sec = substr($sec, 8); $root = 'dropbox'; } try { $oauth = new Dropbox_Curl($sec, $oauth2_id, $key, $dropbox_storage, $callback, $callbackhome, $deauthenticate, $instance_id); } catch (Exception $e) { $this->log("Curl error: ".$e->getMessage()); $this->log(sprintf(__("%s error: %s", 'updraftplus'), "Dropbox/Curl", $e->getMessage().' ('.get_class($e).') (line: '.$e->getLine().', file: '.$e->getFile()).')', 'error'); return false; } if ($deauthenticate) return true; $storage = new UpdraftPlus_Dropbox_API($oauth, $root); $this->set_storage($storage); return $storage; } } pcloud.php 0000644 00000001326 15231511727 0006551 0 ustar 00 <?php if (!defined('UPDRAFTPLUS_DIR')) die('No direct access.'); if (class_exists('UpdraftPlus_Addons_RemoteStorage_pcloud')) { class UpdraftPlus_BackupModule_pcloud extends UpdraftPlus_Addons_RemoteStorage_pcloud { public function __construct() { parent::__construct('pcloud', 'pCloud', false, 'pcloud-logo.png'); } } } else { updraft_try_include_file('methods/addon-not-yet-present.php', 'include_once'); /** * N.B. UpdraftPlus_BackupModule_AddonNotYetPresent extends UpdraftPlus_BackupModule */ class UpdraftPlus_BackupModule_pcloud extends UpdraftPlus_BackupModule_AddonNotYetPresent { public function __construct() { parent::__construct('pcloud', 'pCloud', false, 'pcloud-logo.png'); } } } openstack.php 0000644 00000001103 15231511727 0007243 0 ustar 00 <?php if (!defined('UPDRAFTPLUS_DIR')) die('No direct access.'); // Necessary to place the code in a separate file, because it uses namespaces, which cause a fatal error in PHP 5.2 if (version_compare(phpversion(), '5.3.3', '>=')) { updraft_try_include_file('methods/openstack2.php', 'include_once'); } else { updraft_try_include_file('methods/insufficient.php', 'include_once'); class UpdraftPlus_BackupModule_openstack extends UpdraftPlus_BackupModule_insufficientphp { public function __construct() { parent::__construct('openstack', 'OpenStack', '5.3.3'); } } } addon-base-v2.php 0000644 00000030116 15231511727 0007604 0 ustar 00 <?php if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed'); /* Methods to define when extending this class (can use $this->storage and $this->options where relevant): do_bootstrap($possible_options_array) # Return a WP_Error object if something goes wrong do_upload($file, $sourcefile) # Return true/false do_listfiles($match) do_delete($file) - return true/false do_download($file, $fullpath, $start_offset) - return true/false do_config_print() get_credentials_test_required_parameters() - return an array: keys = required _POST parameters; values = description of each do_credentials_test($testfile, $posted_settings) - return true/false; or alternatively an array with keys 'result' (true/false) and 'data' (arbitrary debug data) do_credentials_test_deletefile($testfile, $posted_settings) */ // Uses job options: Yes // Uses single-array storage: Yes if (!class_exists('UpdraftPlus_BackupModule')) updraft_try_include_file('methods/backup-module.php', 'require_once'); /** * Note that the naming of this class is historical. There is nothing inherent which restricts it to add-ons, or requires add-ons to use it. It is just an abstraction layer that results in needing to write less code for the storage module. */ abstract class UpdraftPlus_RemoteStorage_Addons_Base_v2 extends UpdraftPlus_BackupModule { protected $method; protected $description; protected $options; private $chunked; /** * Decides whether to print the test button * * @var Boolean */ protected $test_button; public function __construct($method, $description, $chunked = true, $test_button = true) { $this->method = $method; $this->description = $description; $this->chunked = $chunked; $this->test_button = $test_button; } /** * download method: takes a file name (base name), and removes it from the cloud storage * * @param String $file specific file for being removed from cloud storage * @return Array */ public function download($file) { return $this->download_file(false, $file); } public function backup($backup_array) { return $this->upload_files(null, $backup_array); } public function delete($files, $method_obj = false, $sizeinfo = array()) { return $this->delete_files(false, $files, $method_obj, $sizeinfo); } protected function required_configuration_keys() { } public function upload_files($ret, $backup_array) {// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Unused parameter is present because the caller from this class uses 2 arguments. global $updraftplus; $this->options = $this->get_options(); if (!$this->options_exist($this->options)) { $this->log('No settings were found'); $this->log(sprintf(__('No %s settings were found', 'updraftplus'), $this->description), 'error'); return false; } $storage = $this->bootstrap(); if (is_wp_error($storage)) return $updraftplus->log_wp_error($storage, false, true); $this->set_storage($storage); $updraft_dir = trailingslashit($updraftplus->backups_dir_location()); foreach ($backup_array as $file) { $this->log("upload ".((!empty($this->options['ownername'])) ? '(account owner: '.$this->options['ownername'].')' : '').": attempt: $file"); try { if ($this->do_upload($file, $updraft_dir.$file)) { $updraftplus->uploaded_file($file); } else { $any_failures = true; $this->log('ERROR: Failed to upload file: '.$file); $this->log(__('Error', 'updraftplus').': '.$this->description.': '.sprintf(__('Failed to upload %s', 'updraftplus'), $file), 'error'); } } catch (Exception $e) { $any_failures = true; $this->log('ERROR ('.get_class($e).'): '.$file.': Failed to upload file: '.$e->getMessage().' (code: '.$e->getCode().', line: '.$e->getLine().', file: '.$e->getFile().')'); $this->log(__('Error', 'updraftplus').': '.$this->description.': '.sprintf(__('Failed to upload %s', 'updraftplus'), $file), 'error'); } } return (!empty($any_failures)) ? null : true; } /** * This function lists the files found in the configured storage location * * @param String $match a substring to require * * @return Array|WP_Error - each file is represented by an array with entries 'name' and (optional) 'size' */ public function listfiles($match = 'backup_') { try { if (!method_exists($this, 'do_listfiles')) { return new WP_Error('no_listing', 'This remote storage method does not support file listing'); } $this->options = $this->get_options(); if (!$this->options_exist($this->options)) return new WP_Error('no_settings', sprintf(__('No %s settings were found', 'updraftplus'), $this->description)); $storage = $this->bootstrap(); if (is_wp_error($storage)) return $storage; return $this->do_listfiles($match); } catch (Exception $e) { $this->log('ERROR: Failed to list files: '.$e->getMessage().' (code: '.$e->getCode().', line: '.$e->getLine().', file: '.$e->getFile().')'); return new WP_Error('list_failed', $this->description.': '.__('failed to list files', 'updraftplus')); } } /** * This function handles bootstrapping and calling the remote methods delete function * * @param Boolean $ret - A boolean value * @param Array $files - An array of files to delete. * * @return - On success returns true, false or WordPress Error on failure */ public function delete_files($ret, $files) { global $updraftplus; if (is_string($files)) $files = array($files); if (empty($files)) return true; if (!method_exists($this, 'do_delete')) { $this->log("Delete failed: this storage method does not allow deletions"); return false; } $storage = $this->get_storage(); if (empty($storage)) { $this->options = $this->get_options(); if (!$this->options_exist($this->options)) { $this->log('No settings were found'); $this->log(sprintf(__('No %s settings were found', 'updraftplus'), $this->description), 'error'); return false; } $storage = $this->bootstrap(); if (is_wp_error($storage)) return $storage; } $ret = true; if ($this->supports_feature('multi_delete')) { $updraftplus->log("Delete remote files: ".implode(', ', $files)); try { $responses = $this->do_delete($files); $ret = $this->process_multi_delete_responses($files, $responses); } catch (Exception $e) { $updraftplus->log('ERROR:'.implode($files).': Failed to delete files: '.$e->getMessage().' (code: '.$e->getCode().', line: '.$e->getLine().', file: '.$e->getFile().')'); $ret = false; } return $ret; } foreach ($files as $file) { $this->log("Delete remote: $file"); try { $ret = $this->do_delete($file); if (true === $ret) { $this->log("$file: Delete succeeded"); } else { $this->log("Delete failed"); } } catch (Exception $e) { $this->log('ERROR: '.$file.': Failed to delete file: '.$e->getMessage().' (code: '.$e->getCode().', line: '.$e->getLine().', file: '.$e->getFile().')'); $ret = false; } } return $ret; } public function download_file($ret, $files) { global $updraftplus; if (is_string($files)) $files = array($files); if (empty($files)) return true; if (!method_exists($this, 'do_download')) { $this->log("Download failed: this storage method does not allow downloading"); $this->log(__('This storage method does not allow downloading', 'updraftplus'), 'error'); return false; } $this->options = $this->get_options(); if (!$this->options_exist($this->options)) { $this->log('No settings were found'); $this->log(sprintf(__('No %s settings were found', 'updraftplus'), $this->description), 'error'); return false; } try { $storage = $this->bootstrap(); if (is_wp_error($storage)) return $updraftplus->log_wp_error($storage, false, true); } catch (Exception $e) { $ret = false; $this->log('ERROR: '.$files[0].': Failed to download file: '.$e->getMessage().' (code: '.$e->getCode().', line: '.$e->getLine().', file: '.$e->getFile().')'); $this->log(__('Error', 'updraftplus').': '.$this->description.': '.sprintf(__('Failed to download %s', 'updraftplus'), $files[0]), 'error'); } $ret = true; $updraft_dir = untrailingslashit($updraftplus->backups_dir_location()); foreach ($files as $file) { try { $fullpath = $updraft_dir.'/'.$file; $start_offset = file_exists($fullpath) ? filesize($fullpath) : 0; if (false == $this->do_download($file, $fullpath, $start_offset)) { $ret = false; $this->log("error: failed to download: $file"); $this->log("$file: ".sprintf(__("%s Error", 'updraftplus'), $this->description).": ".__('Failed to download', 'updraftplus'), 'error'); } } catch (Exception $e) { $ret = false; $this->log('ERROR: '.$file.': Failed to download file: '.$e->getMessage().' (code: '.$e->getCode().', line: '.$e->getLine().', file: '.$e->getFile().')'); $this->log(__('Error', 'updraftplus').': '.$this->description.': '.sprintf(__('Failed to download %s', 'updraftplus'), $file), 'error'); } } return $ret; } /** * Get the configuration template * * @return String - the template, ready for substitutions to be carried out */ public function get_configuration_template() { $template_str = ''; if (method_exists($this, 'do_get_configuration_template')) { $template_str .= $this->do_get_configuration_template(); } if (!$this->test_button || (method_exists($this, 'should_print_test_button') && !$this->should_print_test_button())) return $template_str; $template_str .= $this->get_test_button_html($this->description); return $template_str; } /** * Modifies handerbar template options * * @param array $opts * @return Array - Modified handerbar template options */ public function transform_options_for_template($opts) { if (method_exists($this, 'do_transform_options_for_template')) { $opts = $this->do_transform_options_for_template($opts); } return $opts; } public function config_print_javascript_onready() { $this->do_config_javascript(); } protected function do_config_javascript() { } /** * Analyse the passed-in options to indicate whether something is configured or not. * * @param Array $opts - options to examine * * @return Boolean */ public function options_exist($opts) { if (is_array($opts) && !empty($opts)) return true; return false; } public function bootstrap($opts = false, $connect = true) { if (false === $opts) $opts = $this->options; $storage = $this->get_storage(); // Be careful of checking empty($opts) here - some storage methods may have no options until the OAuth token has been obtained if ($connect && !$this->options_exist($opts)) return new WP_Error('no_settings', sprintf(__('No %s settings were found', 'updraftplus'), $this->description)); if (!empty($storage) && !is_wp_error($storage)) return $storage; return $this->do_bootstrap($opts, $connect); } /** * Run a credentials test. Output can be echoed. * * @param Array $posted_settings - settings to use * * @return Mixed - any data to return (gets logged in the browser eventually) */ public function credentials_test($posted_settings) { $required_test_parameters = $this->get_credentials_test_required_parameters(); foreach ($required_test_parameters as $param => $descrip) { if (empty($posted_settings[$param])) { echo esc_html(sprintf(__("Failure: No %s was given.", 'updraftplus'), $descrip))."\n"; return; } } $storage = $this->bootstrap($posted_settings); if (is_wp_error($storage)) { echo esc_html__("Failed", 'updraftplus').": "; foreach ($storage->get_error_messages() as $msg) { echo esc_html($msg) .'\n'; } return; } $testfile = md5(time().rand()).'.txt'; $test_results = $this->do_credentials_test($testfile, $posted_settings); $data = (is_array($test_results) && isset($test_results['data'])) ? $test_results['data'] : null; if ((is_array($test_results) && $test_results['result']) || (!is_array($test_results) && $test_results)) { esc_html_e('Success', 'updraftplus'); $this->do_credentials_test_deletefile($testfile, $posted_settings); } else { esc_html_e("Failed: We were not able to place a file in that directory - please check your credentials.", 'updraftplus'); } return $data; } } dreamobjects.php 0000644 00000025711 15231511727 0007731 0 ustar 00 <?php if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed.'); updraft_try_include_file('methods/s3.php', 'require_once'); /** * Converted to multi-options (Feb 2017-) and previous options conversion removed: Yes */ class UpdraftPlus_BackupModule_dreamobjects extends UpdraftPlus_BackupModule_s3 { // This gets populated in the constructor private $dreamobjects_endpoints = array(); protected $provider_can_use_aws_sdk = false; protected $provider_has_regions = true; /** * Class constructor */ public function __construct() { // When new endpoint introduced in future, Please add it here and also add it as hard coded option for endpoint dropdown in self::get_partial_configuration_template_for_endpoint() // Put the default first $this->dreamobjects_endpoints = array( // Endpoint, then the label 'objects-us-east-1.dream.io' => 'objects-us-east-1.dream.io', 'objects-us-west-1.dream.io' => 'objects-us-west-1.dream.io ('.__('Closing 1st October 2018', 'updraftplus').')', ); } protected $use_v4 = false; /** * Given an S3 object, possibly set the region on it * * @param Object $obj - like UpdraftPlus_S3 * @param String $region - or empty to fetch one from saved configuration * @param String $bucket_name */ protected function set_region($obj, $region = '', $bucket_name = '') {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $bucket_name $config = $this->get_config(); $endpoint = ('' != $region && 'n/a' != $region) ? $region : $config['endpoint']; global $updraftplus; if ($updraftplus->backup_time) { $updraftplus->log("Set endpoint (".get_class($obj)."): $endpoint"); // Warning for objects-us-west-1 shutdown in Oct 2018 if ('objects-us-west-1.dream.io' == $endpoint) { $updraftplus->log("The objects-us-west-1.dream.io endpoint shut down on the 1st October 2018. The upload is expected to fail. Please see the following article for more information https://help.dreamhost.com/hc/en-us/articles/360002135871-Cluster-migration-procedure", 'warning', 'dreamobjects_west_shutdown'); } } $obj->setEndpoint($endpoint); } /** * This method overrides the parent method and lists the supported features of this remote storage option. * * @return Array - an array of supported features (any features not mentioned are asuumed to not be supported) */ public function get_supported_features() { // This options format is handled via only accessing options via $this->get_options() return array('multi_options', 'config_templates', 'multi_storage', 'conditional_logic'); } /** * Retrieve default options for this remote storage module. * * @return Array - an array of options */ public function get_default_options() { return array( 'accesskey' => '', 'secretkey' => '', 'path' => '', ); } /** * Retrieve specific options for this remote storage module * * @param Boolean $force_refresh - if set, and if relevant, don't use cached credentials, but get them afresh * * @return Array - an array of options */ protected function get_config($force_refresh = false) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $force_refresh unused $opts = $this->get_options(); $opts['whoweare'] = 'DreamObjects'; $opts['whoweare_long'] = 'DreamObjects'; $opts['key'] = 'dreamobjects'; if (empty($opts['endpoint'])) { $endpoints = array_keys($this->dreamobjects_endpoints); $opts['endpoint'] = $endpoints[0]; } return $opts; } /** * Get the pre configuration template */ public function get_pre_configuration_template() { ?> <tr class="{{get_template_css_classes false}} {{method_display_name}}_pre_config_container"> <td colspan="2"> <a href="https://dreamhost.com/cloud/dreamobjects/" target="_blank"><img alt="{{method_display_name}}" src="{{storage_image_url}}"></a> <br> {{{xmlwriter_existence_label}}} {{{simplexmlelement_existence_label}}} {{{curl_existence_label}}} <br> {{{console_url_text}}} <p> <a href="{{updraftplus_com_link}}" target="_blank">{{ssl_error_text}}</a> </p> </td> </tr> <?php } /** * Get the configuration template * * @return String - the template, ready for substitutions to be carried out */ public function get_configuration_template() { // return $this->get_configuration_template_engine('dreamobjects', 'DreamObjects', 'DreamObjects', 'DreamObjects', 'https://panel.dreamhost.com/index.cgi?tree=storage.dreamhostobjects', '<a href="https://dreamhost.com/cloud/dreamobjects/" target="_blank"><img alt="DreamObjects" src="'.UPDRAFTPLUS_URL.'/images/dreamobjects_logo-horiz-2013.png"></a>'); ob_start(); ?> <tr class="{{get_template_css_classes true}}"> <th>{{input_accesskey_label}}:</th> <td><input class="updraft_input--wide udc-wd-600" data-updraft_settings_test="accesskey" type="text" autocomplete="off" id="{{get_template_input_attribute_value "id" "accesskey"}}" name="{{get_template_input_attribute_value "name" "accesskey"}}" value="{{accesskey}}" /></td> </tr> <tr class="{{get_template_css_classes true}}"> <th>{{input_secretkey_label}}:</th> <td><input class="updraft_input--wide udc-wd-600" data-updraft_settings_test="secretkey" type="{{input_secretkey_type}}" autocomplete="off" id="{{get_template_input_attribute_value "id" "secretkey"}}" name="{{get_template_input_attribute_value "name" "secretkey"}}" value="{{secretkey}}" /></td> </tr> <tr class="{{get_template_css_classes true}}"> <th>{{input_location_label}}:</th> <td>{{method_id}}://<input class="updraft_input--wide udc-wd-600" data-updraft_settings_test="path" title="{{input_location_title}}" type="text" id="{{get_template_input_attribute_value "id" "path"}}" name="{{get_template_input_attribute_value "name" "path"}}" value="{{path}}" /></td> </tr> <tr class="{{get_template_css_classes true}}"> <th>{{input_endpoint_label}}</th> <td> <select data-updraft_settings_test="endpoint" id="{{get_template_input_attribute_value "id" "endpoint"}}" name="{{get_template_input_attribute_value "name" "endpoint"}}" style="width: 360px"> {{#each dreamobjects_endpoints as |description endpoint|}} <option value="{{endpoint}}" {{#ifeq ../endpoint endpoint}}selected="selected"{{/ifeq}}>{{description}}</option> {{/each}} </select> </td> </tr> {{{get_template_test_button_html "DreamObjects"}}} <?php return ob_get_clean(); } /** * Modifies handerbar template options * * @param array $opts * @return Array - Modified handerbar template options */ public function transform_options_for_template($opts) { $opts['endpoint'] = empty($opts['endpoint']) ? '' : $opts['endpoint']; $opts['dreamobjects_endpoints'] = $this->dreamobjects_endpoints; return $opts; } /** * Retrieve a list of template properties by taking all the persistent variables and methods of the parent class and combining them with the ones that are unique to this module, also the necessary HTML element attributes and texts which are also unique only to this backup module * NOTE: Please sanitise all strings that are required to be shown as HTML content on the frontend side (i.e. wp_kses()), or any other technique to prevent XSS attacks that could come via WP hooks * * @return Array an associative array keyed by names that describe themselves as they are */ public function get_template_properties() { global $updraftplus, $updraftplus_admin; $properties = array( 'storage_image_url' => UPDRAFTPLUS_URL."/images/dreamobjects_logo-horiz-2013.png", 'curl_existence_label' => wp_kses($updraftplus_admin->curl_check($updraftplus->backup_methods[$this->get_id()], false, $this->get_id()." hidden-in-updraftcentral", false), $this->allowed_html_for_content_sanitisation()), 'simplexmlelement_existence_label' => !apply_filters('updraftplus_dreamobjects_simplexmlelement_exists', class_exists('SimpleXMLElement')) ? wp_kses($updraftplus_admin->show_double_warning('<strong>'.__('Warning', 'updraftplus').':</strong> '.sprintf(__("Your web server's PHP installation does not included a required module (%s).", 'updraftplus'), 'SimpleXMLElement').' '.__("Please contact your web hosting provider's support.", 'updraftplus').' '.sprintf(__("UpdraftPlus's %s module <strong>requires</strong> %s.", 'updraftplus'), $updraftplus->backup_methods[$this->get_id()], 'SimpleXMLElement').' '.__('Please do not file any support requests; there is no alternative.', 'updraftplus'), $this->get_id(), false), $this->allowed_html_for_content_sanitisation()) : '', 'xmlwriter_existence_label' => !apply_filters('updraftplus_dreamobjects_xmlwriter_exists', 'UpdraftPlus_S3_Compat' != $this->indicate_s3_class() || !class_exists('XMLWriter')) ? wp_kses($updraftplus_admin->show_double_warning('<strong>'.__('Warning', 'updraftplus').':</strong> '. sprintf(__("Your web server's PHP installation does not included a required module (%s).", 'updraftplus'), 'XMLWriter').' '.__("Please contact your web hosting provider's support and ask for them to enable it.", 'updraftplus'), $this->get_id(), false), $this->allowed_html_for_content_sanitisation()) : '', 'console_url_text' => sprintf(__('Get your access key and secret key from your <a href="%s">%s console</a>, then pick a (globally unique - all %s users) bucket name (letters and numbers) (and optionally a path) to use for storage.', 'updraftplus'), 'https://panel.dreamhost.com/index.cgi?tree=storage.dreamhostobjects', $updraftplus->backup_methods[$this->get_id()], $updraftplus->backup_methods[$this->get_id()]).' '.__('This bucket will be created for you if it does not already exist.', 'updraftplus'), 'updraftplus_com_link' => apply_filters("updraftplus_com_link", "https://updraftplus.com/faqs/i-get-ssl-certificate-errors-when-backing-up-andor-restoring/"), 'ssl_error_text' => __('If you see errors about SSL certificates, then please go here for help.', 'updraftplus'), 'credentials_creation_link_text' => __('Create Azure credentials in your Azure developer console.', 'updraftplus'), 'configuration_helper_link_text' => __('For more detailed instructions, follow this link.', 'updraftplus'), 'input_accesskey_label' => sprintf(__('%s access key', 'updraftplus'), $updraftplus->backup_methods[$this->get_id()]), 'input_secretkey_label' => sprintf(__('%s secret key', 'updraftplus'), $updraftplus->backup_methods[$this->get_id()]), 'input_secretkey_type' => apply_filters('updraftplus_admin_secret_field_type', 'password'), 'input_location_label' => sprintf(__('%s location', 'updraftplus'), $updraftplus->backup_methods[$this->get_id()]), 'input_location_title' => __('Enter only a bucket name or a bucket and path.', 'updraftplus').' '.__('Examples: mybucket, mybucket/mypath', 'updraftplus'), 'input_endpoint_label' => sprintf(__('%s end-point', 'updraftplus'), $updraftplus->backup_methods[$this->get_id()]), 'input_test_label' => sprintf(__('Test %s Settings', 'updraftplus'), $updraftplus->backup_methods[$this->get_id()]), ); return wp_parse_args($properties, $this->get_persistent_variables_and_methods()); } } azure.php 0000644 00000002231 15231511727 0006405 0 ustar 00 <?php if (!defined('UPDRAFTPLUS_DIR')) die('No direct access.'); if (version_compare(phpversion(), '5.3.3', '>=')) { if (class_exists('UpdraftPlus_Addons_RemoteStorage_azure')) { class UpdraftPlus_BackupModule_azure extends UpdraftPlus_Addons_RemoteStorage_azure { public function __construct() { parent::__construct('azure', 'Microsoft Azure', true, true); } } } else { updraft_try_include_file('methods/addon-not-yet-present.php', 'include_once'); /** * N.B. UpdraftPlus_BackupModule_AddonNotYetPresent extends UpdraftPlus_BackupModule */ class UpdraftPlus_BackupModule_azure extends UpdraftPlus_BackupModule_AddonNotYetPresent { public function __construct() { parent::__construct('azure', 'Microsoft Azure', '5.3.3', 'azure.png'); } } } } else { updraft_try_include_file('methods/insufficient.php', 'include_once'); /** * N.B. UpdraftPlus_BackupModule_insufficientphp extends UpdraftPlus_BackupModule */ class UpdraftPlus_BackupModule_azure extends UpdraftPlus_BackupModule_insufficientphp { public function __construct() { parent::__construct('azure', 'Microsoft Azure', '5.3.3', 'azure.png'); } } } onedrive.php 0000644 00000002155 15231511727 0007077 0 ustar 00 <?php if (!defined('UPDRAFTPLUS_DIR')) die('No direct access.'); if (version_compare(phpversion(), '5.3.3', '>=')) { if (class_exists('UpdraftPlus_Addons_RemoteStorage_onedrive')) { class UpdraftPlus_BackupModule_onedrive extends UpdraftPlus_Addons_RemoteStorage_onedrive { public function __construct() { parent::__construct('onedrive', 'Microsoft OneDrive', '5.3.3', 'onedrive.png'); } } } else { updraft_try_include_file('methods/addon-not-yet-present.php', 'include_once'); /** * N.B. UpdraftPlus_BackupModule_AddonNotYetPresent extends UpdraftPlus_BackupModule */ class UpdraftPlus_BackupModule_onedrive extends UpdraftPlus_BackupModule_AddonNotYetPresent { public function __construct() { parent::__construct('onedrive', 'Microsoft OneDrive', '5.3.3', 'onedrive.png'); } } } } else { updraft_try_include_file('methods/insufficient.php', 'include_once'); class UpdraftPlus_BackupModule_onedrive extends UpdraftPlus_BackupModule_insufficientphp { public function __construct() { parent::__construct('onedrive', 'Microsoft OneDrive', '5.3.3', 'onedrive.png'); } } } cloudfiles-new.php 0000644 00000037020 15231511727 0010203 0 ustar 00 <?php if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed.'); // SDK uses namespacing - requires PHP 5.3 (actually the SDK states its requirements as 5.3.3) use OpenCloud\Rackspace; // New SDK - https://github.com/rackspace/php-opencloud and http://docs.rackspace.com/sdks/guide/content/php.html // Uploading: https://github.com/rackspace/php-opencloud/blob/master/docs/userguide/ObjectStore/Storage/Object.md updraft_try_include_file('methods/openstack-base.php', 'require_once'); class UpdraftPlus_BackupModule_cloudfiles_opencloudsdk extends UpdraftPlus_BackupModule_openstack_base { public function __construct() { parent::__construct('cloudfiles', 'Cloud Files', 'Rackspace Cloud Files', '/images/rackspacecloud-logo.png'); } public function get_client() { return $this->client; } public function get_openstack_service($opts, $useservercerts = false, $disablesslverify = null) { $user = $opts['user']; $apikey = $opts['apikey']; $authurl = $opts['authurl']; $region = (!empty($opts['region'])) ? $opts['region'] : null; updraft_try_include_file('vendor/autoload.php', 'include_once'); // The new authentication APIs don't match the values we were storing before $new_authurl = ('https://lon.auth.api.rackspacecloud.com' == $authurl || 'uk' == $authurl) ? Rackspace::UK_IDENTITY_ENDPOINT : Rackspace::US_IDENTITY_ENDPOINT; if (null === $disablesslverify) $disablesslverify = UpdraftPlus_Options::get_updraft_option('updraft_ssl_disableverify'); if (empty($user) || empty($apikey)) throw new Exception(__('Authorisation failed (check your credentials)', 'updraftplus')); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Error message to be escaped when caught and printed. $this->log("authentication URL: ".$new_authurl); $client = new Rackspace($new_authurl, array( 'username' => $user, 'apiKey' => $apikey )); $this->client = $client; if ($disablesslverify) { $client->setSslVerification(false); } else { if ($useservercerts) { $client->setConfig(array($client::SSL_CERT_AUTHORITY, 'system')); } else { $client->setSslVerification(UPDRAFTPLUS_DIR.'/includes/cacert.pem', true, 2); } } return $client->objectStoreService('cloudFiles', $region); } /** * This method overrides the parent method and lists the supported features of this remote storage option. * * @return Array - an array of supported features (any features not * mentioned are assumed to not be supported) */ public function get_supported_features() { // This options format is handled via only accessing options via $this->get_options() return array('multi_options', 'config_templates', 'multi_storage', 'conditional_logic'); } /** * Retrieve default options for this remote storage module. * * @return Array - an array of options */ public function get_default_options() { return array( 'user' => '', 'authurl' => 'https://auth.api.rackspacecloud.com', 'apikey' => '', 'path' => '', 'region' => null ); } /** * This gives the partial template string to the settings page for the CloudFiles settings. * * @return String - the partial template, ready for substitutions to be carried out */ public function get_configuration_middlesection_template() { global $updraftplus; $classes = $this->get_css_classes(); $template_str = ' <tr class="'.$classes.'"> <th title="'.__('Accounts created at rackspacecloud.com are US accounts; accounts created at rackspace.co.uk are UK accounts.', 'updraftplus').'">'.__('US or UK-based Rackspace Account', 'updraftplus').':</th> <td> <select data-updraft_settings_test="authurl" '.$this->output_settings_field_name_and_id('authurl', true).' title="'.__('Accounts created at rackspacecloud.com are US-accounts; accounts created at rackspace.co.uk are UK-based', 'updraftplus').'"> <option {{#ifeq "https://auth.api.rackspacecloud.com" authurl}}selected="selected"{{/ifeq}} value="https://auth.api.rackspacecloud.com">'.__('US (default)', 'updraftplus').'</option> <option {{#ifeq "https://lon.auth.api.rackspacecloud.com" authurl}}selected="selected"{{/ifeq}} value="https://lon.auth.api.rackspacecloud.com">'.__('UK', 'updraftplus').'</option> </select> </td> </tr> <tr class="'.$classes.'"> <th>'.__('Cloud Files Storage Region', 'updraftplus').':</th> <td> <select data-updraft_settings_test="region" '.$this->output_settings_field_name_and_id('region', true).'> {{#each regions as |desc reg|}} <option {{#ifeq ../region reg}}selected="selected"{{/ifeq}} value="{{reg}}">{{desc}}</option> {{/each}} </select> </td> </tr> <tr class="'.$classes.'"> <th>'.__('Cloud Files Username', 'updraftplus').':</th> <td><input data-updraft_settings_test="user" type="text" autocomplete="off" class="updraft_input--wide" '.$this->output_settings_field_name_and_id('user', true).' value="{{user}}" /> <div style="clear:both;"> '.apply_filters('updraft_cloudfiles_apikeysetting', '<a href="'.$updraftplus->get_url('premium').'" target="_blank"><em>'.__('To create a new Rackspace API sub-user and API key that has access only to this Rackspace container, use Premium.', 'updraftplus').'</em></a>').' </div> </td> </tr> <tr class="'.$classes.'"> <th>'.__('Cloud Files API Key', 'updraftplus').':</th> <td><input data-updraft_settings_test="apikey" type="'.apply_filters('updraftplus_admin_secret_field_type', 'password').'" autocomplete="off" class="updraft_input--wide" '.$this->output_settings_field_name_and_id('apikey', true).' value="{{apikey}}" /> </td> </tr> <tr class="'.$classes.'"> <th>'.apply_filters('updraftplus_cloudfiles_location_description', __('Cloud Files Container', 'updraftplus')).':</th> <td><input data-updraft_settings_test="path" type="text" class="updraft_input--wide" '.$this->output_settings_field_name_and_id('path', true).' value="{{path}}" /></td> </tr>'; return $template_str; } /** * Modifies handerbar template options * * @param array $opts handerbar template options * @return Array - Modified handerbar template options */ public function transform_options_for_template($opts) { $opts['regions'] = array( 'DFW' => __('Dallas (DFW) (default)', 'updraftplus'), 'SYD' => __('Sydney (SYD)', 'updraftplus'), 'ORD' => __('Chicago (ORD)', 'updraftplus'), 'IAD' => __('Northern Virginia (IAD)', 'updraftplus'), 'HKG' => __('Hong Kong (HKG)', 'updraftplus'), 'LON' => __('London (LON)', 'updraftplus') ); $opts['region'] = (empty($opts['region'])) ? 'DFW' : $opts['region']; if (isset($opts['apikey'])) { $opts['apikey'] = trim($opts['apikey']); } $opts['authurl'] = !empty($opts['authurl']) ? $opts['authurl'] : ''; return $opts; } /** * Perform a test of user-supplied credentials, and echo the result * * @param Array $posted_settings - settings to test */ public function credentials_test($posted_settings) { if (empty($posted_settings['apikey'])) { echo esc_html(sprintf(__("Failure: No %s was given.", 'updraftplus'), __('API key', 'updraftplus'))); return; } if (empty($posted_settings['user'])) { echo esc_html(sprintf(__("Failure: No %s was given.", 'updraftplus'), __('Username', 'updraftplus'))); return; } $opts = array( 'user' => $posted_settings['user'], 'apikey' => $posted_settings['apikey'], 'authurl' => $posted_settings['authurl'], 'region' => (empty($posted_settings['region'])) ? null : $posted_settings['region'] ); $this->credentials_test_go($opts, $posted_settings['path'], $posted_settings['useservercerts'], $posted_settings['disableverify']); } /** * Check whether options have been set up by the user, or not * * @param Array $opts - the potential options * * @return Boolean */ public function options_exist($opts) { if (is_array($opts) && isset($opts['user']) && '' != $opts['user'] && !empty($opts['apikey'])) return true; return false; } /** * Get the pre configuration template * * @return String - the template */ public function get_pre_configuration_template() { ?> <tr class="{{get_template_css_classes false}} {{method_id}}_pre_config_container"> <td colspan="2"> {{#if storage_image_url}} <img alt="{{storage_long_description}}" src="{{storage_image_url}}"> {{/if}} <br> {{{mb_substr_existence_label}}} {{{json_last_error_existence_label}}} {{{curl_existence_label}}} <br> <p>{{{rackspace_text_description}}} <a href="{{faq_link_url}}" target="_blank">{{faq_link_text}}</a></p> </td> </tr> <?php } /** * Get the configuration template * * @return String - the template, ready for substitutions to be carried out */ public function get_configuration_template() { ob_start(); ?> <tr class="{{get_template_css_classes true}}"> <th title="{{input_account_title}}">{{input_account_label}}:</th> <td> <select data-updraft_settings_test="authurl" class="udc-wd-600" id="{{get_template_input_attribute_value "id" "authurl"}}" name="{{get_template_input_attribute_value "name" "authurl"}}" title="{{input_account_title}}"> {{#each input_account_option_labels}} <option {{#ifeq ../authurl @key}}selected="selected"{{/ifeq}} value="{{@key}}">{{this}}</option> {{/each}} </select> </td> </tr> <tr class="{{get_template_css_classes true}}"> <th>{{input_region_label}}:</th> <td> <select data-updraft_settings_test="region" class="udc-wd-600" id="{{get_template_input_attribute_value "id" "region"}}" name="{{get_template_input_attribute_value "name" "region"}}"> {{#each regions as |desc reg|}} <option {{#ifeq ../region reg}}selected="selected"{{/ifeq}} value="{{reg}}">{{desc}}</option> {{/each}} </select> </td> </tr> <tr class="{{get_template_css_classes true}}"> <th>{{input_username_label}}:</th> <td><input data-updraft_settings_test="user" type="text" autocomplete="off" class="updraft_input--wide udc-wd-600" id="{{get_template_input_attribute_value "id" "user"}}" name="{{get_template_input_attribute_value "name" "user"}}" value="{{user}}" /> <div style="clear:both;"> {{{input_username_title}}} </div> </td> </tr> <tr class="{{get_template_css_classes true}}"> <th>{{input_apikey_label}}:</th> <td><input data-updraft_settings_test="apikey" type="{{input_apikey_type}}" autocomplete="off" class="updraft_input--wide udc-wd-600" id="{{get_template_input_attribute_value "id" "apikey"}}" name="{{get_template_input_attribute_value "name" "apikey"}}" value="{{apikey}}" /> </td> </tr> <tr class="{{get_template_css_classes true}}"> <th>{{input_container_label}}:</th> <td><input data-updraft_settings_test="path" type="text" class="updraft_input--wide udc-wd-600" id="{{get_template_input_attribute_value "id" "path"}}" name="{{get_template_input_attribute_value "name" "path"}}" value="{{path}}" /></td> </tr>'; {{{get_template_test_button_html "Rackspace Cloud Files"}}} <?php return ob_get_clean(); } /** * Retrieve a list of template properties by taking all the persistent variables and methods of the parent class and combining them with the ones that are unique to this module, also the necessary HTML element attributes and texts which are also unique only to this backup module * NOTE: Please sanitise all strings that are required to be shown as HTML content on the frontend side (i.e. wp_kses()), or any other technique to prevent XSS attacks that could come via WP hooks * * @return Array an associative array keyed by names that describe themselves as they are */ public function get_template_properties() { global $updraftplus, $updraftplus_admin; $properties = array( 'storage_image_url' => !empty($this->img_url) ? UPDRAFTPLUS_URL.$this->img_url : '', 'storage_long_description' => $this->long_desc, 'mb_substr_existence_label' => !apply_filters('updraftplus_openstack_mbsubstr_exists', function_exists('mb_substr')) ? wp_kses($updraftplus_admin->show_double_warning('<strong>'.__('Warning', 'updraftplus').':</strong> '.sprintf(__('Your web server\'s PHP installation does not include a required module (%s).', 'updraftplus'), 'mbstring').' '.__('Please contact your web hosting provider\'s support.', 'updraftplus').' '.sprintf(__("UpdraftPlus's %s module <strong>requires</strong> %s.", 'updraftplus'), $this->desc, 'mbstring').' '.__('Please do not file any support requests; there is no alternative.', 'updraftplus'), $this->method, false), $this->allowed_html_for_content_sanitisation()) : '', 'json_last_error_existence_label' => !apply_filters('updraftplus_rackspace_jsonlasterror_exists', function_exists('json_last_error')) ? wp_kses($updraftplus_admin->show_double_warning('<strong>'.__('Warning', 'updraftplus').':</strong> '.sprintf(__('Your web server\'s PHP installation does not include a required module (%s).', 'updraftplus'), 'json').' '.__('Please contact your web hosting provider\'s support.', 'updraftplus').' '.sprintf(__("UpdraftPlus's %s module <strong>requires</strong> %s.", 'updraftplus'), 'Cloud Files', 'json').' '.__('Please do not file any support requests; there is no alternative.', 'updraftplus'), 'cloudfiles', false), $this->allowed_html_for_content_sanitisation()) : '', 'curl_existence_label' => wp_kses($updraftplus_admin->curl_check($this->long_desc, false, $this->method.' hidden-in-updraftcentral', false), $this->allowed_html_for_content_sanitisation()), 'rackspace_text_description' => wp_kses(sprintf(__('Get your API key <a href="%s" target="_blank">from your Rackspace Cloud console</a> (<a href="%s" target="_blank">read instructions here</a>), then pick a container name to use for storage.', 'updraftplus'), 'https://mycloud.rackspace.com/', 'https://docs.rackspace.com/support/how-to/set-up-an-api-key-cloud-office-control-panel').' '.__('This container will be created for you if it does not already exist.', 'updraftplus'), $this->allowed_html_for_content_sanitisation()), 'faq_link_text' => __('Also, you should read this important FAQ.', 'updraftplus'), 'faq_link_url' => wp_kses(apply_filters("updraftplus_com_link", "https://updraftplus.com/faqs/there-appear-to-be-lots-of-extra-files-in-my-rackspace-cloud-files-container/"), array(), array('http', 'https')), 'input_account_label' => __('US or UK-based Rackspace Account', 'updraftplus'), 'input_account_title' => __('Accounts created at rackspacecloud.com are US accounts; accounts created at rackspace.co.uk are UK accounts.', 'updraftplus'), 'input_account_option_labels' => array( 'https://auth.api.rackspacecloud.com' => __('US (default)', 'updraftplus'), 'https://lon.auth.api.rackspacecloud.com' => __('UK', 'updraftplus'), ), 'input_region_label' => __('Cloud Files Storage Region', 'updraftplus'), 'input_username_label' => __('Cloud Files Username', 'updraftplus'), 'input_username_title' => wp_kses(apply_filters('updraft_cloudfiles_apikeysetting', '<a href="'.$updraftplus->get_url('premium').'" target="_blank"><em>'.__('To create a new Rackspace API sub-user and API key that has access only to this Rackspace container, use Premium.', 'updraftplus').'</em></a>'), $this->allowed_html_for_content_sanitisation()), 'input_apikey_label' => __('Cloud Files API Key', 'updraftplus'), 'input_apikey_type' => apply_filters('updraftplus_admin_secret_field_type', 'password'), 'input_container_label' => wp_kses(apply_filters('updraftplus_cloudfiles_location_description', __('Cloud Files Container', 'updraftplus')), $this->allowed_html_for_content_sanitisation()), 'input_test_label' => sprintf(__('Test %s Settings', 'updraftplus'), $updraftplus->backup_methods[$this->get_id()]), ); return wp_parse_args($properties, $this->get_persistent_variables_and_methods()); } } googledrive.php 0000644 00000216233 15231511727 0007576 0 ustar 00 <?php if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed.'); // Converted to multi-options (Feb 2017-) and previous options conversion removed: Yes if (!class_exists('UpdraftPlus_BackupModule')) updraft_try_include_file('methods/backup-module.php', 'require_once'); class UpdraftPlus_BackupModule_googledrive extends UpdraftPlus_BackupModule { private $client; private $ids_from_paths = array(); private $client_id; private $callback_url; private $multi_directories = array(); private $registered_prune = array(); /** * Constructor */ public function __construct() { $this->client_id = defined('UPDRAFTPLUS_GOOGLEDRIVE_CLIENT_ID') ? UPDRAFTPLUS_GOOGLEDRIVE_CLIENT_ID : '916618189494-u3ehb1fl7u3meb63nb2b4fqi0r9pcfe2.apps.googleusercontent.com'; $this->callback_url = defined('UPDRAFTPLUS_GOOGLEDRIVE_CALLBACK_URL') ? UPDRAFTPLUS_GOOGLEDRIVE_CALLBACK_URL : 'https://auth.updraftplus.com/auth/googledrive'; if (class_exists('UpdraftPlus_Addon_Google_Enhanced')) { add_action('updraftplus_admin_enqueue_scripts', array($this, 'admin_footer_jstree')); } } public function action_auth() { if (isset($_GET['state'])) { $parts = explode(':', $_GET['state']); $state = $parts[0]; if ('success' == $state) { if (isset($_GET['user_id']) && isset($_GET['access_token'])) { $code = array( 'user_id' => $_GET['user_id'], 'access_token' => $_GET['access_token'] ); if (isset($_GET['scope'])) { $scope = $_GET['scope']; $code['scope'] = explode(' ', $scope); } } else { $code = array(); } $this->do_complete_authentication($state, $code); } elseif ('token' == $state) { $this->gdrive_auth_token(); } elseif ('revoke' == $state) { $this->gdrive_auth_revoke(); } } elseif (isset($_GET['updraftplus_googledriveauth'])) { if ('doit' == $_GET['updraftplus_googledriveauth']) { $this->action_authenticate_storage(); } elseif ('deauth' == $_GET['updraftplus_googledriveauth']) { $this->action_deauthenticate_storage(); } } } /** * This method overrides the parent method and lists the supported features of this remote storage option. * * @return Array - an array of supported features (any features not mentioned are asuumed to not be supported) */ public function get_supported_features() { // This options format is handled via only accessing options via $this->get_options() return array('multi_options', 'config_templates', 'multi_storage', 'conditional_logic', 'manual_authentication'); } /** * Retrieve default options for this remote storage module. * * @return Array - an array of options */ public function get_default_options() { // parentid is deprecated since April 2014; it should not be in the default options (its presence is used to detect an upgraded-from-previous-SDK situation). For the same reason, 'folder' is also unset; which enables us to know whether new-style settings have ever been set. return array( 'clientid' => '', 'secret' => '', 'token' => '', ); } /** * Check whether options have been set up by the user, or not * * @param Array $opts - the potential options * * @return Boolean */ public function options_exist($opts) { if (is_array($opts) && (!empty($opts['user_id']) || !empty($opts['token']))) return true; return false; } /** * Get the Google folder ID for the root of the drive * * @return String|Integer */ private function root_id() { return $this->get_storage()->about->get()->getRootFolderId(); } /** * Get folder id from path * * @param String $path folder path * @param Boolean $one_only if false, then will be returned as a list (Google Drive allows multiple entities with the same name) * @param Integer $retry_count how many times to retry upon a network failure * * @return Array|String|Integer|Boolean internal id of the Google Drive folder (or list of them if $one_only was false), or false upon failure */ public function id_from_path($path, $one_only = true, $retry_count = 3) { $storage = $this->get_storage(); try { while ('/' == substr($path, 0, 1)) { $path = substr($path, 1); } $cache_key = empty($path) ? '/' : ($one_only ? $path : 'multi:'.$path); if (isset($this->ids_from_paths[$this->get_instance_id()][$cache_key])) return $this->ids_from_paths[$this->get_instance_id()][$cache_key]; $current_parent_id = $this->root_id(); $current_path = '/'; if (!empty($path)) { $nodes = explode('/', $path); foreach ($nodes as $element) { $found = array(); $sub_items = $this->get_subitems($current_parent_id, 'dir', $element); foreach ($sub_items as $item) { try { if ($item->getTitle() == $element) { $current_path .= $element.'/'; $current_parent_id = $item->getId(); $found[$current_parent_id] = strtotime($item->getCreatedDate()); } } catch (Exception $e) { $this->log("id_from_path: exception: ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); } } if (count($found) > 1) { asort($found); reset($found); $current_parent_id = key($found); } elseif (empty($found)) { $ref = new UDP_Google_Service_Drive_ParentReference; $ref->setId($current_parent_id); $dir = new UDP_Google_Service_Drive_DriveFile(); $dir->setMimeType('application/vnd.google-apps.folder'); $dir->setParents(array($ref)); $dir->setTitle($element); $this->log('creating path: '.$current_path.$element); $dir = $storage->files->insert( $dir, array('mimeType' => 'application/vnd.google-apps.folder') ); $current_path .= $element.'/'; $current_parent_id = $dir->getId(); } } } if (empty($this->ids_from_paths[$this->get_instance_id()])) $this->ids_from_paths[$this->get_instance_id()] = array(); $this->ids_from_paths[$this->get_instance_id()][$cache_key] = ($one_only || empty($found) || 1 == count($found)) ? $current_parent_id : $found; return $this->ids_from_paths[$this->get_instance_id()][$cache_key]; } catch (Exception $e) { $msg = $e->getMessage(); $this->log("id_from_path failure: exception (".get_class($e)."): ".$msg.' (line: '.$e->getLine().', file: '.$e->getFile().')'); if (is_a($e, 'UDP_Google_Service_Exception') && false !== strpos($msg, 'Invalid json in service response') && function_exists('mb_strpos')) { // Aug 2015: saw a case where the gzip-encoding was not removed from the result // https://stackoverflow.com/questions/10975775/how-to-determine-if-a-string-was-compressed // @codingStandardsIgnoreLine $is_gzip = (false !== mb_strpos($msg, "\x1f\x8b\x08")); if ($is_gzip) $this->log("Error: Response appears to be gzip-encoded still; something is broken in the client HTTP stack, and you should define UPDRAFTPLUS_GOOGLEDRIVE_DISABLEGZIP as true in your wp-config.php to overcome this."); } $retry_count--; $this->log("id_from_path: retry ($retry_count)"); if ($retry_count > 0) { $delay_in_seconds = defined('UPDRAFTPLUS_GOOGLE_DRIVE_GET_FOLDER_ID_SECOND_RETRY_DELAY') ? UPDRAFTPLUS_GOOGLE_DRIVE_GET_FOLDER_ID_SECOND_RETRY_DELAY : 5-$retry_count; sleep($delay_in_seconds); return $this->id_from_path($path, $one_only, $retry_count); } return false; } } /** * Runs upon the WP action updraftplus_prune_retained_backups_finished */ public function prune_retained_backups_finished() { if (empty($this->multi_directories[$this->get_instance_id()]) || count($this->multi_directories[$this->get_instance_id()]) < 2) return; $storage = $this->bootstrap(); if (false == $storage || is_wp_error($storage)) return; foreach (array_keys($this->multi_directories[$this->get_instance_id()]) as $drive_id) { if (!isset($oldest_reference)) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable -- The variable is defined below. $oldest_id = $drive_id; $oldest_reference = new UDP_Google_Service_Drive_ParentReference; $oldest_reference->setId($oldest_id); continue; } // All found files should be moved to the oldest folder try { $sub_items = $this->get_subitems($drive_id, 'file'); } catch (Exception $e) { $this->log('list files: failed to access chosen folder: '.$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); } $without_errors = true; foreach ($sub_items as $item) { $title = "(unknown)"; try { $title = $item->getTitle(); $this->log("Moving: $title (".$item->getId().") from duplicate folder $drive_id to $oldest_id"); $file = new UDP_Google_Service_Drive_DriveFile(); $file->setParents(array($oldest_reference)); $storage->files->patch($item->getId(), $file); } catch (Exception $e) { $this->log("move: exception: ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); $without_errors = false; continue; } } if ($without_errors) { if (!empty($sub_items)) { try { $sub_items = $this->get_subitems($drive_id, 'file'); } catch (Exception $e) { $this->log('list files: failed to access chosen folder: '.$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); } } if (empty($sub_items)) { try { $storage->files->delete($drive_id); $this->log("removed empty duplicate folder ($drive_id)"); } catch (Exception $e) { $this->log("delete empty duplicate folder ($drive_id): exception: ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); continue; } } } } } /** * Get the Google Drive internal ID * * @param Array $opts - storage instance options * @param Boolean $one_only - whether to potentially return them all if there is more than one * * @return String|Array */ private function get_parent_id($opts, $one_only = true) { $storage = $this->get_storage(); $filtered = apply_filters('updraftplus_googledrive_parent_id', false, $opts, $storage, $this, $one_only); if (!empty($filtered)) return $filtered; if (isset($opts['parentid'])) { if (empty($opts['parentid'])) { return $this->root_id(); } else { $parent = is_array($opts['parentid']) ? $opts['parentid']['id'] : $opts['parentid']; } } else { $folder = !empty($opts['folder']) ? $opts['folder'] : 'UpdraftPlus'; $parent = $this->id_from_path($folder, $one_only); } return empty($parent) ? $this->root_id() : $parent; } /** * List files or folders on Google Drive. * * @param string $match The file or folder name to search for (default: 'backup_'). * @param bool $list_files Whether to search for files (true) or folders (false) (default: true). * * @return array|WP_Error An array of results containing information about matching files or folders, * or a WP_Error object if there are missing settings, * or an error occurs while accessing Google Drive. * Each result is represented as an associative array with the following keys: * - 'name' (string): The name of the file or folder. * - 'size' (int, optional): The size of the file in bytes (only for files). * - 'id' (string, optional): The unique identifier of the folder (only for folders). */ public function list_files_or_folders($match = 'backup_', $list_files = true) { $opts = $this->get_options(); $use_master = $this->use_master($opts); if (!$use_master) { if (empty($opts['secret']) || empty($opts['clientid']) || empty($opts['clientid'])) return new WP_Error('no_settings', sprintf(__('No %s settings were found', 'updraftplus'), __('Google Drive', 'updraftplus'))); } else { if (empty($opts['user_id']) || empty($opts['tmp_access_token'])) return new WP_Error('no_settings', sprintf(__('No %s settings were found', 'updraftplus'), __('Google Drive', 'updraftplus'))); } $storage = $this->bootstrap(); if (is_wp_error($storage) || false == $storage) return $storage; try { if ($list_files) { $parent_id = $this->get_parent_id($opts); $sub_items = $this->get_subitems($parent_id, 'file'); } else { $sub_items = $this->get_subitems($match, 'dir', ''); } } catch (Exception $e) { return new WP_Error(__('Google Drive list files: failed to access parent folder', 'updraftplus').": ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); } $results = array(); foreach ($sub_items as $item) { $title = "(unknown)"; try { $title = $item->getTitle(); if ($list_files) { if (0 === strpos($title, $match)) { $results[] = array('name' => $title, 'size' => $item->getFileSize()); } } else { $results[] = array('name' => $title, 'id' => $item->getId()); } } catch (Exception $e) { $this->log("list: exception: ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); continue; } } return $results; } /** * List folders in Google Drive. * * @param string $search - The folder to search for (default: 'root'). * @return array $results - An array of folder information. */ public function list_folders($search = 'root') { return $this->list_files_or_folders($search, false); } /** * List files in Google Drive. * * @param string $search - The files to search for (default: 'backup_'). * @return array $results - An array of file information. */ public function listfiles($search = 'backup_') { return $this->list_files_or_folders($search); } /** * Get a Google account access token using the refresh token * * @param String $refresh_token Specify refresh token * @param String $client_id Specify Client ID * @param String $client_secret Specify client secret * @return Boolean */ private function access_token($refresh_token, $client_id, $client_secret) { $this->log("requesting access token: client_id=$client_id"); $query_body = array( 'refresh_token' => $refresh_token, 'client_id' => $client_id, 'client_secret' => $client_secret, 'grant_type' => 'refresh_token' ); $result = wp_remote_post('https://accounts.google.com/o/oauth2/token', array( 'timeout' => '20', 'method' => 'POST', 'body' => $query_body ) ); if (is_wp_error($result)) { $this->log("error when requesting access token"); foreach ($result->get_error_messages() as $msg) $this->log("Error message: $msg"); return false; } else { $json_values = json_decode(wp_remote_retrieve_body($result), true); if (isset($json_values['access_token'])) { $this->log("successfully obtained access token"); return $json_values['access_token']; } else { $response = json_decode($result['body'], true); if (!empty($response['error']) && 'deleted_client' == $response['error']) { $this->log(__('The client has been deleted from the Google Drive API console.', 'updraftplus').' '.__('Please create a new Google Drive project and reconnect with UpdraftPlus.', 'updraftplus'), 'error'); } $error_code = empty($response['error']) ? 'no error code' : $response['error']; $this->log("error ($error_code) when requesting access token: response does not contain access_token. Response: ".(is_string($result['body']) ? str_replace("\n", '', $result['body']) : json_encode($result['body']))); return false; } } } /** * This method will return a redirect URL depending on the parameter passed. It will either return the redirect for the user's site or the auth server. * * @param Boolean $master - indicate whether we want the master redirect URL * @return String - a redirect URL */ private function redirect_uri($master = false) { if ($master) { return $this->callback_url; } else { return UpdraftPlus_Options::admin_page_url().'?action=updraftmethod-googledrive-auth'; } } /** * Acquire single-use authorization code from Google via OAuth 2.0 * * @param String $instance_id - the instance id of the settings we want to authenticate */ public function do_authenticate_storage($instance_id) { $opts = $this->get_options(); $use_master = $this->use_master($opts); // First, revoke any existing token, since Google doesn't appear to like issuing new ones if (!empty($opts['token']) && !$use_master) $this->gdrive_auth_revoke(); // Set a flag so we know this authentication is in progress $opts['auth_in_progress'] = true; $this->set_options($opts, true); $prefixed_instance_id = ':' . $instance_id; // We use 'force' here for the approval_prompt, not 'auto', as that deals better with messy situations where the user authenticated, then changed settings if ($use_master) { $client_id = $this->client_id; $token = 'token'.$prefixed_instance_id; $token .= $this->redirect_uri(); } else { $client_id = $opts['clientid']; $token = 'token'.$prefixed_instance_id; } // We require access to all Google Drive files (not just ones created by this app - scope https://www.googleapis.com/auth/drive.file) - because we need to be able to re-scan storage for backups uploaded by other installs, or manually by the user into their Google Drive. But, if you are happy to lose that capability, you can use the filter below to remove the drive.readonly scope. $params = array( 'response_type' => 'code', 'client_id' => $client_id, 'redirect_uri' => $this->redirect_uri($use_master), // Nov 2024 - https://www.googleapis.com/auth/drive.readonly temporarily removed for annual re-verification (for which we received no notification) 'scope' => apply_filters('updraft_googledrive_scope', 'https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/userinfo.profile'), 'state' => $token, 'access_type' => 'offline', // 'approval_prompt' => 'force', // legacy and has been deprected. It can lead to conflicts when specified along with "prompt" param // Nov 2024 - changed from `true` for the same reason as a few lines earlier 'include_granted_scopes' => 'false', 'enable_granular_consent' => 'true', 'prompt' => 'select_account consent', // new option param as the replacement to 'approval_prompt' ); $params = apply_filters('updraft_googledrive_auth_params', $params); if (headers_sent()) { $this->log(sprintf(__('The %s authentication could not go ahead, because something else on your site is breaking it.', 'updraftplus'), 'Google Drive').' '.__('Try disabling your other plugins and switching to a default theme.', 'updraftplus').' ('.__('Specifically, you are looking for the component that sends output (most likely PHP warnings/errors) before the page begins.', 'updraftplus').' '.__('Turning off any debugging settings may also help).', 'updraftplus').')', 'error'); } else { header('Location: https://accounts.google.com/o/oauth2/auth?'.http_build_query($params, '', '&')); } } /** * This function will complete the oAuth flow, if return_instead_of_echo is true then add the action to display the authed admin notice, otherwise echo this notice to page. * * @param string $state - the state * @param string $code - the oauth code * @param boolean $return_instead_of_echo - a boolean to indicate if we should return the result or echo it * * @return void|string - returns the authentication message if return_instead_of_echo is true */ public function do_complete_authentication($state, $code, $return_instead_of_echo = false) { // If these are set then this is a request from our master app and the auth server has returned these to be saved. if (isset($code['user_id']) && isset($code['access_token'])) { $opts = $this->get_options(); $opts['user_id'] = base64_decode($code['user_id']); $opts['tmp_access_token'] = base64_decode($code['access_token']); if (isset($opts['auth_in_progress'])) $opts['scope'] = $code['scope']; // Unset this value if it is set as this is a fresh auth we will set this value in the next step if (isset($opts['expires_in'])) unset($opts['expires_in']); // remove our flag so we know this authentication is complete if (isset($opts['auth_in_progress'])) unset($opts['auth_in_progress']); $this->set_options($opts, true); } if ($return_instead_of_echo) { return $this->show_authed_admin_success($return_instead_of_echo); } else { add_action('all_admin_notices', array($this, 'show_authed_admin_success')); } } /** * Revoke a Google account refresh token * Returns the parameter fed in, so can be used as a WordPress options filter * Can be called statically from UpdraftPlus::googledrive_clientid_checkchange() * * @param Boolean $unsetopt unset options is set to true unless otherwise specified */ public function gdrive_auth_revoke($unsetopt = true) { $opts = $this->get_options(); $result = wp_remote_get('https://accounts.google.com/o/oauth2/revoke?token='.$opts['token']); // If the call to revoke the token fails, we try again one more time if (is_wp_error($result) || 200 != wp_remote_retrieve_response_code($result)) { wp_remote_get('https://accounts.google.com/o/oauth2/revoke?token='.$opts['token']); } if ($unsetopt) { $opts['token'] = ''; unset($opts['ownername']); $this->set_options($opts, true); } } /** * Get a Google account refresh token using the code received from do_authenticate_storage */ public function gdrive_auth_token() { $opts = $this->get_options(); if (isset($_GET['code'])) { $post_vars = array( 'code' => $_GET['code'], 'client_id' => $opts['clientid'], 'client_secret' => $opts['secret'], 'redirect_uri' => UpdraftPlus_Options::admin_page_url().'?action=updraftmethod-googledrive-auth', 'grant_type' => 'authorization_code' ); $result = wp_remote_post('https://accounts.google.com/o/oauth2/token', array('timeout' => 25, 'method' => 'POST', 'body' => $post_vars)); if (is_wp_error($result)) { $add_to_url = "Bad response when contacting Google: "; foreach ($result->get_error_messages() as $message) { $this->log("authentication error: ".$message); $add_to_url .= $message.". "; } header('Location: '.UpdraftPlus_Options::admin_page_url().'?page=updraftplus&error='.urlencode($add_to_url)); } else { $json_values = json_decode(wp_remote_retrieve_body($result), true); if (isset($json_values['refresh_token'])) { // Save token $opts['token'] = $json_values['refresh_token']; $this->set_options($opts, true); if (isset($json_values['access_token'])) { $opts['tmp_access_token'] = $json_values['access_token']; $this->set_options($opts, true); // We do this to clear the GET parameters, otherwise WordPress sticks them in the _wp_referer in the form and brings them back, leading to confusion + errors header('Location: '.UpdraftPlus_Options::admin_page_url().'?action=updraftmethod-googledrive-auth&page=updraftplus&state=success:'.urlencode($this->get_instance_id())); } } else { $msg = __('No refresh token was received from Google.', 'updraftplus').' '.__('This often means that you entered your client secret wrongly, or that you have not yet re-authenticated (below) since correcting it.', 'updraftplus').' '.__('Re-check it, then follow the link to authenticate again.', 'updraftplus').' '.__('Finally, if that does not work, then use expert mode to wipe all your settings, create a new Google client ID/secret, and start again.', 'updraftplus'); if (isset($json_values['error'])) $msg .= ' '.sprintf(__('Error: %s', 'updraftplus'), $json_values['error']); header('Location: '.UpdraftPlus_Options::admin_page_url().'?page=updraftplus&error='.urlencode($msg)); } } } else { header('Location: '.UpdraftPlus_Options::admin_page_url().'?page=updraftplus&error='.urlencode(sprintf(__('%s authorization failed', 'updraftplus'), 'Google Drive'))); } } /** * This method will setup the authenticated admin warning, it can either return this or echo it * * @param boolean $return_instead_of_echo - a boolean to indicate if we should return the result or echo it * * @return void|string - returns the authentication message if return_instead_of_echo is true */ public function show_authed_admin_success($return_instead_of_echo) { global $updraftplus_admin; $opts = $this->get_options(); if (empty($opts['tmp_access_token'])) return; $updraftplus_tmp_access_token = $opts['tmp_access_token']; $message = ''; try { $storage = $this->bootstrap($updraftplus_tmp_access_token); if (false != $storage && !is_wp_error($storage)) { $about = $storage->about->get(); $quota_total = max($about->getQuotaBytesTotal(), 1); $quota_used = $about->getQuotaBytesUsed(); $username = $about->getName(); $get_user = $about->getUser(); $email = is_object($get_user) ? $get_user->emailAddress : ''; $opts['ownername'] = $username; $opts['owneremail'] = $email; if (is_numeric($quota_total) && is_numeric($quota_used)) { $available_quota = $quota_total - $quota_used; $used_perc = round($quota_used*100/$quota_total, 1); $message .= sprintf(__('Your %s quota usage: %s %% used, %s available', 'updraftplus'), 'Google Drive', $used_perc, round($available_quota/1048576, 1).' MB'); } } elseif (is_wp_error($storage)) { $message .= __('However, subsequent access attempts failed:', 'updraftplus'); $error_codes = $storage->get_error_codes(); $message .= '<ul style="list-style: disc inside;">'; foreach ($error_codes as $error_code) { $message .= '<li>'; $message .= $storage->get_error_message($error_code).' ('.$error_code.')'; $message .= '</li>'; } $message .= '</ul>'; } } catch (Exception $e) { if (is_a($e, 'UDP_Google_Service_Exception')) { $errs = $e->getErrors(); $message .= __('However, subsequent access attempts failed:', 'updraftplus'); if (is_array($errs)) { $message .= '<ul style="list-style: disc inside;">'; foreach ($errs as $err) { $message .= '<li>'; if (!empty($err['reason'])) $message .= '<strong>'.htmlspecialchars($err['reason']).':</strong> '; if (!empty($err['message'])) { $message .= htmlspecialchars($err['message']); } else { $message .= htmlspecialchars(serialize($err)); } $message .= '</li>'; } $message .= '</ul>'; } else { $message .= htmlspecialchars(serialize($errs)); } } } unset($opts['tmp_access_token']); $this->set_options($opts, true); $final_message = __('Success', 'updraftplus').': '.sprintf(__('you have authenticated your %s account.', 'updraftplus'), __('Google Drive', 'updraftplus')).' '.((!empty($username)) ? sprintf(__('Name: %s.', 'updraftplus'), $username).' ' : '').$message; if ($return_instead_of_echo) { return "<div class='updraftmessage updated'><p>{$final_message}</p></div>"; } else { $updraftplus_admin->show_admin_warning($final_message); } } /** * This function just does the formalities, and off-loads the main work to upload_file * * @param array $backup_array */ public function backup($backup_array) { global $updraftplus; $storage = $this->bootstrap(); if (false == $storage || is_wp_error($storage)) return $storage; $updraft_dir = trailingslashit($updraftplus->backups_dir_location()); $opts = $this->get_options(); try { $parent_ids = $this->get_parent_id($opts, false); if (is_array($parent_ids)) { reset($parent_ids); $parent_id = key($parent_ids); if (count($parent_ids) > 1) { $this->log('there appears to be more than one folder: '.implode(', ', array_keys($parent_ids))); if (empty($this->registered_prune[$this->get_instance_id()])) { $this->registered_prune[$this->get_instance_id()] = true; $this->multi_directories[$this->get_instance_id()] = $parent_ids; add_action('updraftplus_prune_retained_backups_finished', array($this, 'prune_retained_backups_finished')); } } } else { $parent_id = $parent_ids; } } catch (Exception $e) { $this->log("upload: failed to access parent folder: ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); $this->log(sprintf(__('Failed to upload to %s', 'updraftplus'), __('Google Drive', 'updraftplus')).': '.__('failed to access parent folder', 'updraftplus').' ('.$e->getMessage().')', 'error'); return false; } foreach ($backup_array as $file) { $available_quota = -1; do { try { $try_again = false; $about = $storage->about->get(); $quota_total = max($about->getQuotaBytesTotal(), 1); $quota_used = $about->getQuotaBytesUsed(); $available_quota = $quota_total - $quota_used; $message = "quota usage: used=".round($quota_used/1048576, 1)." MB, total=".round($quota_total/1048576, 1)." MB, available=".round($available_quota/1048576, 1)." MB"; $this->log($message); } catch (Exception $e) { $msg = $e->getMessage(); $this->log("quota usage: failed to obtain this information: ".$msg); // If the issue was a problem refreshing the OAuth2 token, bootstrap again and try again if (false !== strpos($msg, 'Error refreshing the OAuth2 token')) { $this->log("quota usage: will attempt to refresh OAuth2 token and fetch this information again"); $this->set_storage(null); $storage = $this->bootstrap(); if (false == $storage || is_wp_error($storage)) return $storage; $try_again = true; } } } while ($try_again); $file_path = $updraft_dir.$file; $file_name = basename($file_path); $this->log("$file_name: Attempting to upload to Google Drive (into folder id: $parent_id)"); $filesize = filesize($file_path); $already_failed = false; if (-1 != $available_quota) { if ($filesize > $available_quota) { $already_failed = true; $this->log("File upload expected to fail: file ($file_name) size is $filesize b, whereas available quota is only $available_quota b"); $this->log(sprintf(__("Account full: your %s account has only %d bytes left, but the file to be uploaded is %d bytes", 'updraftplus'), __('Google Drive', 'updraftplus'), $available_quota, $filesize), 'error'); } } if (!$already_failed && $filesize > 10737418240) { // 10GB $this->log("File upload expected to fail: file ($file_name) size is $filesize b (".round($filesize/1073741824, 4)." GB), whereas Google Drive's limit is 10GB (1073741824 bytes)"); $this->log(sprintf(__("Upload expected to fail: the %s limit for any single file is %s, whereas this file is %s GB (%d bytes)", 'updraftplus'), __('Google Drive', 'updraftplus'), '10GB (1073741824)', round($filesize/1073741824, 4), $filesize), 'warning'); } do { try { $try_again = false; $timer_start = microtime(true); if ($this->upload_file($file_path, $parent_id)) { $this->log('OK: Archive ' . $file_name . ' uploaded in ' . (round(microtime(true) - $timer_start, 2)) . ' seconds'); $updraftplus->uploaded_file($file); } else { $this->log("ERROR: $file_name: Failed to upload"); $this->log("$file_name: ".sprintf(__('Failed to upload to %s', 'updraftplus'), __('Google Drive', 'updraftplus')), 'error'); } } catch (Exception $e) { $msg = $e->getMessage(); $this->log("Upload exception (".get_class($e)."): $msg (line: ".$e->getLine().', file: '.$e->getFile().')'); // If the issue was a problem refreshing the OAuth2 token, bootstrap again and try again if (false !== ($p = strpos($msg, 'Error refreshing the OAuth2 token'))) { $this->log("$file_name: will attempt to refresh OAuth2 token and upload again"); $this->set_storage(null); $storage = $this->bootstrap(); if (false == $storage || is_wp_error($storage)) return $storage; $try_again = true; } else { if (false !== ($p = strpos($msg, 'The user has exceeded their Drive storage quota'))) { $this->log("$file_name: ".sprintf(__('Failed to upload to %s', 'updraftplus'), __('Google Drive', 'updraftplus')).': '.substr($msg, $p), 'error'); } else { $this->log("$file_name: ".sprintf(__('Failed to upload to %s', 'updraftplus'), __('Google Drive', 'updraftplus')), 'error'); } $this->client->setDefer(false); } } } while ($try_again); } return null; } public function bootstrap($access_token = false) { $storage = $this->get_storage(); if (!empty($storage) && is_object($storage) && is_a($storage, 'UDP_Google_Service_Drive')) return $storage; $opts = $this->get_options(); $use_master = $this->use_master($opts); $curl_exists = function_exists('curl_version') && function_exists('curl_exec'); if (!$use_master) { if (empty($opts['token']) || empty($opts['clientid']) || empty($opts['secret'])) { $this->log('this account is not authorised'); $this->log(__('Account is not authorized.', 'updraftplus'), 'error', 'googledrivenotauthed'); return new WP_Error('not_authorized', __('Account is not authorized.', 'updraftplus').' (Google Drive)'); } if (empty($access_token)) { $access_token = $this->access_token($opts['token'], $opts['clientid'], $opts['secret']); } } else { if (empty($opts['user_id'])) { $this->log('this account is not authorised'); $this->log(__('Account is not authorized.', 'updraftplus'), 'error', 'googledrivenotauthed'); return new WP_Error('not_authorized', __('Account is not authorized.', 'updraftplus')); } if (!isset($opts['expires_in']) || $opts['expires_in'] < time()) { $user_id = empty($opts['user_id']) ? '' : $opts['user_id']; $args = array( 'code' => 'ud_googledrive_code', 'user_id' => $user_id, ); $result = wp_remote_post($this->callback_url, array( 'timeout' => 60, 'headers' => apply_filters('updraftplus_auth_headers', ''), 'body' => $args )); if (is_wp_error($result)) { $body = array('result' => 'error', 'error' => $result->get_error_code(), 'error_description' => $result->get_error_message()); } else { $response_code = wp_remote_retrieve_response_code($result); if ($response_code < 200 || $response_code >= 300) { $body = array('result' => 'error', 'error' => $response_code, 'error_description' => sprintf(__("%s for %s", 'updraftplus'), wp_remote_retrieve_response_message($result), $this->callback_url)); } else { $body_json = wp_remote_retrieve_body($result); $body = json_decode($body_json, true); } } if (!empty($body['result']) && 'error' == $body['result']) { $access_token = new WP_Error($body['error'], empty($body['error_description']) ? __('Have not yet obtained an access token from Google - you need to authorise or re-authorise your connection to Google Drive.', 'updraftplus') : $body['error_description']); } else { $result_body_json = base64_decode($body[0]); $result_body = json_decode($result_body_json); if (isset($result_body->access_token)) { $access_token = array( 'access_token' => $result_body->access_token, 'created' => time(), 'expires_in' => $result_body->expires_in, 'refresh_token' => '' ); $opts['tmp_access_token'] = $access_token; $opts['expires_in'] = $access_token['created'] + $access_token['expires_in'] - 30; $this->set_options($opts, true); } else { $access_token = ''; } } } else { $access_token = $opts['tmp_access_token']; } } // Do we have an access token? if (empty($access_token) || is_wp_error($access_token)) { $message = 'ERROR: Have not yet obtained an access token from Google (has the user authorised?)'; $extra = ''; if (is_wp_error($access_token)) { $message .= ' ('.$access_token->get_error_message().') ('.$access_token->get_error_code().')'; $extra = ' ('.$access_token->get_error_message().') ('.$access_token->get_error_code().')'; } $this->log($message); $this->log(__('Have not yet obtained an access token from Google - you need to authorise or re-authorise your connection to Google Drive.', 'updraftplus').$extra, 'error'); return $access_token; } $spl = spl_autoload_functions(); if (is_array($spl)) { // Workaround for Google Drive CDN plugin's autoloader if (in_array('wpbgdc_autoloader', $spl)) spl_autoload_unregister('wpbgdc_autoloader'); // http://www.wpdownloadmanager.com/download/google-drive-explorer/ - but also others, since this is the default function name used by the Google SDK if (in_array('google_api_php_client_autoload', $spl)) spl_autoload_unregister('google_api_php_client_autoload'); } if ((!class_exists('UDP_Google_Config') || !class_exists('UDP_Google_Client') || !class_exists('UDP_Google_Service_Drive') || !class_exists('UDP_Google_Http_Request')) && !function_exists('google_api_php_client_autoload_updraftplus')) { updraft_try_include_file('includes/Google/autoload.php', 'include_once'); } if (!class_exists('UpdraftPlus_Google_Http_MediaFileUpload')) { updraft_try_include_file('includes/google-extensions.php', 'include_once'); } $config = new UDP_Google_Config(); $config->setClassConfig('UDP_Google_IO_Abstract', 'request_timeout_seconds', 60); // In our testing, $storage->about->get() fails if gzip is not disabled when using the stream wrapper if (!$curl_exists || (defined('UPDRAFTPLUS_GOOGLEDRIVE_DISABLEGZIP') && UPDRAFTPLUS_GOOGLEDRIVE_DISABLEGZIP)) { $config->setClassConfig('UDP_Google_Http_Request', 'disable_gzip', true); } if (!$use_master) { $client_id = $opts['clientid']; $client_secret = $opts['secret']; } else { $client_id = $this->client_id; $client_secret = ''; } $proxy = new WP_HTTP_Proxy(); $client = new UDP_Google_Client($config); $is_proxy_enabled = false; if ($proxy->is_enabled() && $proxy->send_through_proxy($client->getBasePath())) { $is_proxy_enabled = true; if ($curl_exists && !defined('CURLOPT_PROXY')) { $this->log('cURL transports couldn\'t be used because a proxy is set but the installed cURL version doesn\'t support proxy connections. Stream/socket transports (UDP_Google_IO_Stream) are being used instead.'); $config->setIoClass('UDP_Google_IO_Stream'); // Redeclare the client to use UDP_Google_IO_Stream $client = new UDP_Google_Client($config); } } $client->setClientId($client_id); $client->setClientSecret($client_secret); // $client->setUseObjects(true); if (!$use_master) { $client->setAccessToken(json_encode(array( 'access_token' => $access_token, 'refresh_token' => $opts['token'] ))); } else { $client->setAccessToken(json_encode($access_token)); } $io = $client->getIo(); $setopts = array(); if (is_a($io, 'UDP_Google_IO_Curl')) { $setopts[CURLOPT_SSL_VERIFYPEER] = UpdraftPlus_Options::get_updraft_option('updraft_ssl_disableverify') ? false : true; if (!UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts')) $setopts[CURLOPT_CAINFO] = UPDRAFTPLUS_DIR.'/includes/cacert.pem'; // Raise the timeout from the default of 15 $setopts[CURLOPT_TIMEOUT] = 60; $setopts[CURLOPT_CONNECTTIMEOUT] = 15; if (defined('UPDRAFTPLUS_IPV4_ONLY') && UPDRAFTPLUS_IPV4_ONLY) $setopts[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V4; $setopts[CURLOPT_HTTP_VERSION] = defined('UPDRAFTPLUS_GDRIVE_CURL_HTTP_VERSION') ? UPDRAFTPLUS_GDRIVE_CURL_HTTP_VERSION : CURL_HTTP_VERSION_1_1; if ($is_proxy_enabled) { $port = (int) $proxy->port(); if (empty($port)) $port = 8080; $setopts[CURLOPT_PROXY] = $proxy->host(); $setopts[CURLOPT_PROXYPORT] = $port; $setopts[CURLOPT_PROXYTYPE] = CURLPROXY_HTTP; } } elseif (is_a($io, 'UDP_Google_IO_Stream')) { $setopts['timeout'] = 60; // We had to modify the SDK to support this // https://wiki.php.net/rfc/tls-peer-verification - before PHP 5.6, there is no default CA file if (!UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts') || (version_compare(PHP_VERSION, '5.6.0', '<'))) $setopts['cafile'] = UPDRAFTPLUS_DIR.'/includes/cacert.pem'; if (UpdraftPlus_Options::get_updraft_option('updraft_ssl_disableverify')) $setopts['disable_verify_peer'] = true; if ($is_proxy_enabled) { $port = (int) $proxy->port(); if (empty($port)) $port = 8080; $setopts['proxy'] = $proxy->host().':'.$port; } } $io->setOptions($setopts); $storage = new UDP_Google_Service_Drive($client); $this->client = $client; $this->set_storage($storage); try { // Get the folder name, if not previously known (this is for the legacy situation where an id, not a name, was stored) if (!empty($opts['parentid']) && (!is_array($opts['parentid']) || empty($opts['parentid']['name']))) { $rootid = $this->root_id(); $title = ''; $parentid = is_array($opts['parentid']) ? $opts['parentid']['id'] : $opts['parentid']; while ((!empty($parentid) && $parentid != $rootid)) { $resource = $storage->files->get($parentid); $title = ($title) ? $resource->getTitle().'/'.$title : $resource->getTitle(); $parents = $resource->getParents(); if (is_array($parents) && count($parents)>0) { $parent = array_shift($parents); $parentid = is_a($parent, 'UDP_Google_Service_Drive_ParentReference') ? $parent->getId() : false; } else { $parentid = false; } } if (!empty($title)) { $opts['parentid'] = array( 'id' => (is_array($opts['parentid']) ? $opts['parentid']['id'] : $opts['parentid']), 'name' => $title ); $this->set_options($opts, true); } } } catch (Exception $e) { $this->log("failed to obtain name of parent folder: ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); } return $storage; } /** * Acts as a WordPress options filter * * @param Array $google - An array of Google Drive options * @return Array - the returned array can either be the set of updated Google Drive settings or a WordPress error array */ public function options_filter($google) { global $updraftplus; // Get the current options (and possibly update them to the new format) $opts = UpdraftPlus_Storage_Methods_Interface::update_remote_storage_options_format('googledrive'); if (is_wp_error($opts)) { if ('recursion' !== $opts->get_error_code()) { $msg = "(".$opts->get_error_code()."): ".$opts->get_error_message(); $this->log($msg); error_log("UpdraftPlus: Google Drive: $msg"); } // The saved options had a problem; so, return the new ones return $google; } // $opts = UpdraftPlus_Options::get_updraft_option('updraft_googledrive'); if (!is_array($google)) return $opts; // Remove instances that no longer exist foreach ($opts['settings'] as $instance_id => $storage_options) { if (!isset($google['settings'][$instance_id])) unset($opts['settings'][$instance_id]); } if (empty($google['settings'])) return $opts; foreach ($google['settings'] as $instance_id => $storage_options) { if (empty($opts['settings'][$instance_id]['user_id'])) { $old_client_id = (empty($opts['settings'][$instance_id]['clientid'])) ? '' : $opts['settings'][$instance_id]['clientid']; if (!empty($opts['settings'][$instance_id]['token']) && $old_client_id != $storage_options['clientid']) { updraft_try_include_file('methods/googledrive.php', 'include_once'); $updraftplus->register_wp_http_option_hooks(); $googledrive = new UpdraftPlus_BackupModule_googledrive(); $googledrive->gdrive_auth_revoke(false); $updraftplus->register_wp_http_option_hooks(false); $opts['settings'][$instance_id]['token'] = ''; unset($opts['settings'][$instance_id]['ownername']); } } foreach ($storage_options as $key => $value) { // Trim spaces - I got support requests from users who didn't spot the spaces they introduced when copy/pasting $opts['settings'][$instance_id][$key] = ('clientid' == $key || 'secret' == $key) ? trim($value) : $value; } if (isset($opts['settings'][$instance_id]['folder'])) { $opts['settings'][$instance_id]['folder'] = apply_filters('updraftplus_options_googledrive_foldername', 'UpdraftPlus', $opts['settings'][$instance_id]['folder']); unset($opts['settings'][$instance_id]['parentid']); } } return $opts; } /** * This function checks if the user has any options for Google Drive saved or if they have defined to use a custom app and if they have we will not use the master Google Drive app and allow them to enter their own client ID and secret * * @param Array $opts - the Google Drive options array * @return Bool - a bool value to indicate if we should use the master app or not */ protected function use_master($opts) { if ((!empty($opts['clientid']) && !empty($opts['secret'])) || (defined('UPDRAFTPLUS_CUSTOM_GOOGLEDRIVE_APP') && UPDRAFTPLUS_CUSTOM_GOOGLEDRIVE_APP)) return false; return true; } /** * Returns array of UDP_Google_Service_Drive_DriveFile objects * * @param String $parent_id This is the Parent ID * @param String $type This is the type of file or directory but by default it is set to 'any' unless specified * @param String $match This will specify which match is used for the SQL but by default it is set to 'backup_' unless specified * * @return Array - list of UDP_Google_Service_Drive_DriveFile items */ private function get_subitems($parent_id, $type = 'any', $match = 'backup_') { $storage = $this->get_storage(); $q = '"'.$parent_id.'" in parents and trashed = false'; if ('dir' == $type) { $q .= ' and mimeType = "application/vnd.google-apps.folder"'; } elseif ('file' == $type) { $q .= ' and mimeType != "application/vnd.google-apps.folder"'; } // We used to use 'contains' in both cases, but this exposed some bug that might be in the SDK or at the Google end - a result that matched for = was not returned with contains if (!empty($match)) { if ('backup_' == $match) { $q .= " and title contains '$match'"; } else { $q .= " and title = '$match'"; } } $result = array(); $page_token = null; do { try { // Default for maxResults is 100 $parameters = array('q' => $q, 'maxResults' => 200); if ($page_token) { $parameters['pageToken'] = $page_token; } $files = $storage->files->listFiles($parameters); $result = array_merge($result, $files->getItems()); $page_token = $files->getNextPageToken(); } catch (Exception $e) { $this->log("get_subitems: An error occurred (will not fetch further): " . $e->getMessage()); $page_token = null; } } while ($page_token); return $result; } /** * Delete a single file from the service using GoogleDrive API * * @param Array|String $files - array of file names to delete * @param Array $data - unused here * @param Array $sizeinfo - unused here * @return Boolean|String - either a boolean true or an error code string */ public function delete($files, $data = null, $sizeinfo = array()) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $data and $sizeinfo unused if (is_string($files)) $files = array($files); $storage = $this->bootstrap(); if (is_wp_error($storage)) { $this->log("delete: failed due to storage error: ".$storage->get_error_code()." (".$storage->get_error_message().")"); return 'service_unavailable'; } if (false == $storage) return $storage; $opts = $this->get_options(); try { $parent_id = $this->get_parent_id($opts); $sub_items = $this->get_subitems($parent_id, 'file'); } catch (Exception $e) { $this->log("delete: failed to access parent folder: ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); return 'container_access_error'; } $ret = true; foreach ($sub_items as $item) { $title = "(unknown)"; try { $title = $item->getTitle(); if (in_array($title, $files)) { $storage->files->delete($item->getId()); $this->log("$title: Deletion successful"); if (($key = array_search($title, $files)) !== false) { unset($files[$key]); } } } catch (Exception $e) { $this->log("delete: exception: ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); $ret = 'file_delete_error'; continue; } } foreach ($files as $file) { $this->log("$file: Deletion failed: file was not found"); } return $ret; } /** * Used internally to upload files * * @param String $file - the full path to the file to upload * @param String $parent_id - the internal Google Drive folder identifier * @param Boolean $try_again - whether to retry in the event of a problem * * @return Boolean - success or failure state */ private function upload_file($file, $parent_id, $try_again = true) { global $updraftplus; $basename = basename($file); $storage = $this->get_storage(); $client = $this->client; // See: https://github.com/google/google-api-php-client/blob/master/examples/fileupload.php (at time of writing, only shows how to upload in chunks, not how to resume) $client->setDefer(true); $local_size = filesize($file); $gdfile = new UDP_Google_Service_Drive_DriveFile(); $gdfile->title = $basename; $ref = new UDP_Google_Service_Drive_ParentReference; $ref->setId($parent_id); $gdfile->setParents(array($ref)); $size = 0; $request = $storage->files->insert($gdfile); $chunk_size = 1048576; $transkey = 'resume_'.md5($file); // This is unset upon completion, so if it is set then we are resuming $possible_location = $this->jobdata_get($transkey, null, 'gd'.$transkey); if (is_array($possible_location)) { $headers = array('content-range' => "bytes */".$local_size); $http_request = new UDP_Google_Http_Request( $possible_location[0], 'PUT', $headers, '' ); $response = $this->client->getIo()->makeRequest($http_request); $can_resume = false; $response_http_code = $response->getResponseHttpCode(); if (200 == $response_http_code || 201 == $response_http_code) { $client->setDefer(false); $this->jobdata_delete($transkey, 'gd'.$transkey); $this->log("$basename: upload appears to be already complete (HTTP code: $response_http_code)"); return true; } if (308 == $response_http_code) { $range = $response->getResponseHeader('range'); if (!empty($range) && preg_match('/bytes=0-(\d+)$/', $range, $matches)) { $can_resume = true; $possible_location[1] = $matches[1]+1; $this->log("$basename: upload already began; attempting to resume from byte ".$matches[1]); } } if (!$can_resume) { $this->log("$basename: upload already began; attempt to resume did not succeed (HTTP code: ".$response_http_code.")"); } } // UpdraftPlus_Google_Http_MediaFileUpload extends Google_Http_MediaFileUpload, with a few extra methods to change private properties to public ones $media = new UpdraftPlus_Google_Http_MediaFileUpload( $client, $request, (('.zip' == substr($basename, -4, 4)) ? 'application/zip' : 'application/octet-stream'), null, true, $chunk_size ); $media->setFileSize($local_size); if (!empty($possible_location)) { // $media->resumeUri = $possible_location[0]; // $media->progress = $possible_location[1]; $media->updraftplus_setResumeUri($possible_location[0]); $media->updraftplus_setProgress($possible_location[1]); $size = $possible_location[1]; } if ($size >= $local_size) return true; $status = false; if (false == ($handle = fopen($file, 'rb'))) { $this->log("failed to open file: $basename"); $this->log("$basename: ".__('Error: Failed to open local file', 'updraftplus'), 'error'); return false; } if ($size > 0 && 0 != fseek($handle, $size)) { $this->log("failed to fseek file: $basename, $size"); $this->log("$basename (fseek): ".__('Error: Failed to open local file', 'updraftplus'), 'error'); return false; } $pointer = $size; try { while (!$status && !feof($handle)) { $chunk = ''; // Google requires chunks of the previous indicated size. Short reads are thus problematic. (Or does it? Was this just because the content-length header was hard-coded to the chunk size? Should be investigated, to see if we can change chunk size dynamically). while (strlen($chunk) < $chunk_size && !feof($handle)) { $chunk .= fread($handle, $chunk_size - strlen($chunk)); } // Do we need any further error handling?? $pointer += strlen($chunk); $start_time = microtime(true); $status = $media->nextChunk($chunk); unset($chunk); $extra_log = $media->getProgress(); if (!$status && $chunk_size < 67108864 && microtime(true) - $start_time < 2.5 && !feof($handle) && $updraftplus->verify_free_memory($chunk_size * 4)) { $memory_usage = round(@memory_get_usage(false)/1048576, 1);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise because of the function. $memory_usage2 = round(@memory_get_usage(true)/1048576, 1);// phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- Silenced to suppress errors that may arise because of the function. $chunk_size = $chunk_size * 2; $extra_log .= ' - increasing chunk size to '.round($chunk_size/1024).' KB'; $extra_log .= " - memory usage: $memory_usage / $memory_usage2"; } $this->jobdata_set($transkey, array($media->updraftplus_getResumeUri(), $media->getProgress())); $updraftplus->record_uploaded_chunk(round(100*$pointer/$local_size, 1), $extra_log, $file); } } catch (UDP_Google_Service_Exception $e) { return $this->catch_upload_engine_exceptions($e, $handle, $try_again, $file, $parent_id); } catch (UDP_Google_IO_Exception $e) { return $this->catch_upload_engine_exceptions($e, $handle, $try_again, $file, $parent_id); } // The final value of $status will be the data from the API for the object // that has been uploaded. $result = (false != $status) ? $status : false;// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- We don't use this at this time. fclose($handle); $client->setDefer(false); $this->jobdata_delete($transkey, 'gd'.$transkey); return true; } /** * This function is used to handle certain exceptions that can rise from uploading files to Google Drive when a retry is possibly desirable. * * @param Exception $e - the Google exception we caught * @param Resource $handle - a file handler object that needs closing * @param Boolean $try_again - indicates if we should try again * @param String $file - the full file path * @param String $parent_id - the Google Drive ID for the parent folder * * @return Boolean */ private function catch_upload_engine_exceptions($e, $handle, $try_again, $file, $parent_id) { $this->log('ERROR: upload exception ('.get_class($e).'): '.$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); $this->client->setDefer(false); fclose($handle); $transkey = $transkey = 'resume_'.md5($file); $this->jobdata_delete($transkey, 'gd'.$transkey); if (false == $try_again) throw $e; // Reset this counter to prevent the something_useful_happened condition's possibility being sent into the far future and potentially missed global $updraftplus; if ($updraftplus->current_resumption > 9) $updraftplus->jobdata_set('uploaded_lastreset', $updraftplus->current_resumption); return $this->upload_file($file, $parent_id, false); } /** * Download method: takes a base name, and brings it back from the cloud storage into the internal directory. * * @param String $file The specific file to be downloaded from the Cloud Storage * * @return Boolean - success or failure state */ public function download($file) { global $updraftplus; $storage = $this->bootstrap(); if (false == $storage || is_wp_error($storage)) return false; global $updraftplus; $opts = $this->get_options(); try { $parent_id = $this->get_parent_id($opts); // $gdparent = $storage->files->get($parent_id); $sub_items = $this->get_subitems($parent_id, 'file'); } catch (Exception $e) { $this->log("delete: failed to access parent folder: ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); return false; } $found = false; foreach ($sub_items as $item) { if ($found) continue; $title = "(unknown)"; try { $title = $item->getTitle(); if ($title == $file) { $gdfile = $item; $found = $item->getId(); $size = $item->getFileSize(); } } catch (Exception $e) { $this->log("download: exception: ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); } } if (false === $found) { $this->log('download: failed: file not found'); $this->log($file.': '.__('Error', 'updraftplus').': '.__('File not found', 'updraftplus'), 'error'); return false; } $download_to = $updraftplus->backups_dir_location().'/'.$file; $existing_size = file_exists($download_to) ? filesize($download_to) : 0; if ($existing_size >= $size) { $this->log('download: was already downloaded ('.filesize($download_to)."/$size bytes)"); return true; } // We only need a chunk size because the API library won't accept a file handle - otherwise, we could download the whole range. But testing (150Mb/s connection) shows that after 32MB almost all the gains have been realised. $chunk_size = 2097152; while ($updraftplus->verify_free_memory($chunk_size * 3) && $chunk_size <= 20971520) { $chunk_size = $chunk_size * 2; } try { while ($existing_size < $size) { $end = min($existing_size + $chunk_size, $size); if ($existing_size > 0) { $put_flag = FILE_APPEND; $headers = array('Range' => 'bytes='.$existing_size.'-'.$end); } else { $put_flag = 0; $headers = ($end < $size) ? array('Range' => 'bytes=0-'.$end) : array(); } $pstart = round(100*$existing_size/$size, 1); $pend = round(100*$end/$size, 1); $this->log("Requesting byte range: $existing_size - $end ($pstart - $pend %)"); $request = $this->client->getAuth()->sign(new UDP_Google_Http_Request($gdfile->getDownloadUrl(), 'GET', $headers, null)); $http_request = $this->client->getIo()->makeRequest($request); $http_response = $http_request->getResponseHttpCode(); if (200 == $http_response || 206 == $http_response) { file_put_contents($download_to, $http_request->getResponseBody(), $put_flag); } else { $this->log("download: failed: unexpected HTTP response code: ".$http_response); $this->log(__("download: failed: file not found", 'updraftplus'), 'error'); return false; } clearstatcache(); $new_size = filesize($download_to); if ($new_size > $existing_size) { $existing_size = $new_size; } else { throw new Exception('Failed to obtain any new data at size: '.$existing_size); } } } catch (Exception $e) { $this->log("download: exception: ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')'); } return true; } /** * Retrieve a list of template properties by taking all the persistent variables and methods of the parent class and combining them with the ones that are unique to this module, also the necessary HTML element attributes and texts which are also unique only to this backup module * NOTE: Please sanitise all strings that are required to be shown as HTML content on the frontend side (i.e. wp_kses()), or any other technique to prevent XSS attacks that could come via WP hooks * * @return Array an associative array keyed by names that describe themselves as they are */ public function get_template_properties() { global $updraftplus; $properties = array( 'storage_image_url' => UPDRAFTPLUS_URL.'/images/googledrive_logo.png', 'storage_image_title' => __('Google Drive', 'updraftplus'), 'input_clientid_label' => __('Google Drive', 'updraftplus').' '.__('Client ID', 'updraftplus'), 'input_clientid_title' => __('If Google later shows you the message "invalid_client", then you did not enter a valid client ID here.', 'updraftplus'), 'input_secret_label' => __('Google Drive', 'updraftplus').' '.__('Client Secret', 'updraftplus'), 'input_secret_type' => apply_filters('updraftplus_admin_secret_field_type', 'password'), 'input_folder_label' => __('Google Drive', 'updraftplus').' '.__('Folder', 'updraftplus'), 'clientid_and_secret_instruction_label1' => __('For longer help, including screenshots, follow this link.', 'updraftplus').' '.__('The description below is sufficient for more expert users.', 'updraftplus'), 'updraftplus_com_link' => apply_filters('updraftplus_com_link', 'https://updraftplus.com/support/configuring-google-drive-api-access-in-updraftplus/'), 'clientid_and_secret_instruction_label2' => __('Follow this link to your Google API Console, and there activate the Drive API and create a Client ID in the API Access section.', 'updraftplus'), 'clientid_and_secret_instruction_label3' => __("Select 'Web Application' as the application type.", 'updraftplus'), 'clientid_and_secret_instruction_label4' => __('You must add the following as the authorised redirect URI (under "More Options") when asked', 'updraftplus'), 'clientid_and_secret_instruction_label5' => UpdraftPlus_Options::admin_page_url().'?action=updraftmethod-googledrive-auth', 'clientid_and_secret_instruction_label6' => __('N.B. If you install UpdraftPlus on several WordPress sites, then you cannot re-use your project; you must create a new one from your Google API console for each site.', 'updraftplus'), 'id_number_instruction_label' => wp_kses(__("<strong>This is NOT a folder name</strong>.", 'updraftplus').' '.__('It is an ID number internal to Google Drive', 'updraftplus'), $this->allowed_html_for_content_sanitisation()), 'custom_folder_name_label' => __('To be able to set a custom folder name, use UpdraftPlus Premium.', 'updraftplus'), 'privacy_policy_label' => wp_kses(sprintf(__('Please read %s for use of our %s authorization app (none of your backup data is sent to us).', 'updraftplus'), '<a target="_blank" href="https://updraftplus.com/faqs/privacy-policy-use-google-drive-app/">'.__('this privacy policy', 'updraftplus').'</a>', 'Google Drive'), $this->allowed_html_for_content_sanitisation()), 'updraftplus_premium_url' => $updraftplus->get_url('premium'), 'authentication_label' => __('Authenticate with Google', 'updraftplus'), 'authentication_label2' => wp_kses(sprintf(__("<strong>After</strong> you have saved your settings (by clicking 'Save Changes' below), then come back here once and follow this link to complete authentication with %s.", 'updraftplus'), $updraftplus->backup_methods[$this->get_id()]), $this->allowed_html_for_content_sanitisation()), 'authentication_link_text' => sprintf(__('Sign in with %s', 'updraftplus'), 'Google'), 'already_authenticated_label' => __("<strong>(You appear to be already authenticated,</strong> though you can authenticate again to refresh your access if you've had a problem).", 'updraftplus'), 'deauthentication_nonce' => wp_create_nonce($this->get_id().'_deauth_nonce'), 'deauthentication_link_text' => sprintf(__("Follow this link to remove these settings for %s.", 'updraftplus'), $updraftplus->backup_methods[$this->get_id()]), 'deauthorise_use_master_label' => __('To de-authorize UpdraftPlus (all sites) from accessing your Google Drive, follow this link to your Google account settings.', 'updraftplus'), 'account_name_label' => __("Account holder's name", 'updraftplus'), ); if (preg_match('#^(https?://(\d+)\.(\d+)\.(\d+)\.(\d+))/#i', apply_filters('updraftplus_gdrive_admin_page_url', UpdraftPlus_Options::admin_page_url()), $matches)) $properties['ip_address'] = $matches[1]; if (isset($properties['ip_address'])) $properties['unallowed_direct_ip_address'] = wp_kses(sprintf(__("%s does not allow authorisation of sites hosted on direct IP addresses.", 'updraftplus').' '.__("You will need to change your site's address (%s) before you can use %s for storage.", 'updraftplus'), __('Google Drive', 'updraftplus'), $matches[1], __('Google Drive', 'updraftplus')), $this->allowed_html_for_content_sanitisation()); return wp_parse_args(apply_filters('updraft_'.$this->get_id().'_template_properties', array()), wp_parse_args($properties, $this->get_persistent_variables_and_methods())); } /** * Get the pre configuration template * * @return String - the template */ public function get_pre_configuration_template() { ?> <tr class="{{get_template_css_classes false}} {{method_id}}_pre_config_container"> <td colspan="2"> <img src="{{storage_image_url}}" alt="{{storage_image_title}}"> {{#unless use_master}} <br> {{#if ip_address}} {{!-- This is advisory - so the fact it doesn't match IPv6 addresses isn't important --}} <p><strong>{{{unallowed_direct_ip_address}}}</strong></p> {{else}} {{!-- If we are not using the master app then show them the instructions for Client ID and Secret --}} <p><a href="{{updraftplus_com_link}}" target="_blank"><strong>{{clientid_and_secret_instruction_label1}}</strong></a></p> <p><a href="https://console.developers.google.com" target="_blank">{{clientid_and_secret_instruction_label2}}</a> {{clientid_and_secret_instruction_label3}}</p> <p>{{clientid_and_secret_instruction_label4}}: <kbd>{{clientid_and_secret_instruction_label5}}</kbd> {{clientid_and_secret_instruction_label6}}</p> {{/if}} {{/unless}} <p> {{{privacy_policy_label}}} </p> </td> </tr> <?php } /** * Get the configuration template * * @return String - the template, ready for substitutions to be carried out */ public function get_configuration_template() { ob_start(); ?> {{#unless use_master}} <tr class="{{get_template_css_classes true}}"> <th>{{input_clientid_label}}:</th> <td><input type="text" autocomplete="off" class="updraft_input--wide" id="{{get_template_input_attribute_value "id" "clientid"}}" name="{{get_template_input_attribute_value "name" "clientid"}}" value="{{clientid}}" /><br><em>{{input_clientid_title}}</em></td> </tr> <tr class="{{get_template_css_classes true}}"> <th>{{input_secret_label}}:</th> <td><input type="{{input_secret_type}}" class="updraft_input--wide" id="{{get_template_input_attribute_value "id" "secret"}}" name="{{get_template_input_attribute_value "name" "secret"}}" value="{{secret}}" /></td> </tr> {{/unless}} {{#if is_google_enhanced_addon}} {{#> gdrive_additional_configuration_top}} {{/gdrive_additional_configuration_top}} {{else}} {{#if parentid}} <tr class="{{get_template_css_classes true}}"> <th>{{input_folder_label}}:</th> <td> <input type="hidden" id="{{get_template_input_attribute_value "id" '["parentid","id"]'}}" name="{{get_template_input_attribute_value "name" '["parentid","id"]'}}" value="{{parentid_str}}"> <input type="text" title="{{parentid_str}}" readonly="readonly" class="updraft_input--wide" value="{{showparent}}"> {{#if is_id_number_instruction}} <em>{{{id_number_instruction_label}}}</em> {{else}} <input type="hidden" id="{{get_template_input_attribute_value "id" '["parentid","name"]'}}" name="{{get_template_input_attribute_value "name" '["parentid","name"]'}}" value="{{parentid.name}}"> {{/if}} {{else}} <tr class="{{get_template_css_classes true}}"> <th>{{input_folder_label}}:</th> <td> <input type="text" readonly="readonly" class="updraft_input--wide" id="{{get_template_input_attribute_value "id" "folder"}}" name="{{get_template_input_attribute_value "name" "folder"}}" value="UpdraftPlus" /> {{/if}} <br> <em> <a href="{{updraftplus_premium_url}}" target="_blank"> {{custom_folder_name_label}} </a> </em> </td> </tr> {{/if}} <tr class="{{get_template_css_classes true}}"> <th>{{authentication_label}}:</th> <td> {{#if is_authenticate_with_google}} <p> {{{already_authenticated_label}}} <a class="updraft_deauthlink" href="{{admin_page_url}}?action=updraftmethod-{{method_id}}-auth&page=updraftplus&updraftplus_{{method_id}}auth=deauth&nonce={{deauthentication_nonce}}&updraftplus_instance={{instance_id}}" data-instance_id="{{instance_id}}" data-remote_method="{{method_id}}">{{deauthentication_link_text}}</a> </p> {{#if use_master}} <p><a target="_blank" href="https://myaccount.google.com/permissions">{{deauthorise_use_master_label}}</a></p> {{/if}} {{/if}} {{#if is_ownername_display}} {{#if owneremail}} <br> {{account_name_label}}: {{ownername}} ({{owneremail}}) {{else}} <br> {{account_name_label}}: {{ownername}} {{/if}} {{/if}} <p> {{{authentication_label2}}} </p> <br> <a data-pretext="{{{authentication_label2}}}" class="button-ud-google updraft_authlink" href="{{admin_page_url}}?&action=updraftmethod-{{method_id}}-auth&page=updraftplus&updraftplus_{{method_id}}auth=doit&nonce={{storage_auth_nonce}}&updraftplus_instance={{instance_id}}" data-instance_id="{{instance_id}}" data-remote_method="{{method_id}}">{{authentication_link_text}}</a> </td> </tr> <?php return ob_get_clean(); } /** * Get partial templates associated to the corresponding backup module (remote storage object) * * @return Array an associative array keyed by names of the partial template */ public function get_partial_templates() { return wp_parse_args(apply_filters('updraft_'.$this->get_id().'_partial_templates', array()), parent::get_partial_templates()); } /** * Modifies handerbar template options * * @param array $opts * @return Array - Modified handerbar template options */ public function transform_options_for_template($opts) { $opts['use_master'] = $this->use_master($opts); $opts['is_google_enhanced_addon'] = class_exists('UpdraftPlus_Addon_Google_Enhanced') ? true : false; if (isset($opts['parentid'])) { $opts['parentid_str'] = (is_array($opts['parentid'])) ? $opts['parentid']['id'] : $opts['parentid']; $opts['showparent'] = (is_array($opts['parentid']) && !empty($opts['parentid']['name'])) ? $opts['parentid']['name'] : $opts['parentid_str']; $opts['is_id_number_instruction'] = (!empty($opts['parentid']) && (!is_array($opts['parentid']) || empty($opts['parentid']['name']))); } $opts['is_authenticate_with_google'] = (!empty($opts['token']) || !empty($opts['user_id'])); $opts['is_ownername_display'] = ((!empty($opts['token']) || !empty($opts['user_id'])) && !empty($opts['ownername'])); $opts = apply_filters('updraftplus_options_googledrive_options', $opts); return $opts; } /** * Gives settings keys which values should not passed to handlebarsjs context. * The settings stored in UD in the database sometimes also include internal information that it would be best not to send to the front-end (so that it can't be stolen by a man-in-the-middle attacker) * * @return Array - Settings array keys which should be filtered */ public function filter_frontend_settings_keys() { return array( 'expires_in', 'tmp_access_token', 'token', 'user_id', ); } /** * This function will build and return the authentication link * * @param String $instance_id - the instance id * @param String $text - the link text * * @return String - the authentication link */ public function build_authentication_link($instance_id, $text) { $id = $this->get_id(); return '<p>'. $text .'</p><br><a data-pretext="'.$text.'" class="button-ud-google updraft_authlink" href="'.UpdraftPlus_Options::admin_page_url().'?&action=updraftmethod-'.$id.'-auth&page=updraftplus&updraftplus_'.$id.'auth=doit&nonce='.wp_create_nonce('storage_auth_nonce').'&updraftplus_instance='.esc_attr($instance_id).'" data-instance_id="'.esc_attr($instance_id).'" data-remote_method="'.$id.'">'.sprintf(__('Sign in with %s', 'updraftplus'), 'Google').'</a>'; } } addon-not-yet-present.php 0000644 00000014223 15231511727 0011423 0 ustar 00 <?php if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed.'); if (!class_exists('UpdraftPlus_BackupModule')) updraft_try_include_file('methods/backup-module.php', 'require_once'); class UpdraftPlus_BackupModule_AddonNotYetPresent extends UpdraftPlus_BackupModule { private $method; private $description; private $required_php; private $image; private $error_msg; private $error_msg_trans; public function __construct($method, $description, $required_php = false, $image = null) { $this->method = $method; $this->description = $description; $this->required_php = $required_php; $this->image = $image; $this->error_msg = 'This remote storage method ('.$this->description.') requires PHP '.$this->required_php.' or later'; $this->error_msg_trans = sprintf(__('This remote storage method (%s) requires PHP %s or later.', 'updraftplus'), $this->description, $this->required_php); } public function backup($backup_array) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Unused variable is present because the function to perform backup for specific storage is not exist. $this->log("You do not have the UpdraftPlus ".$this->method.' add-on installed - get it from '.apply_filters("updraftplus_com_link", "https://updraftplus.com/shop/").''); $this->log(sprintf(__('You do not have the UpdraftPlus %s add-on installed - get it from %s', 'updraftplus'), $this->description, ''.apply_filters("updraftplus_com_link", "https://updraftplus.com/shop/").''), 'error', 'missingaddon-'.$this->method); return false; } /** * Retrieve a list of supported features for this storage method * * Currently known features: * * - multi_options : indicates that the remote storage module * can handle its options being in the Feb-2017 multi-options * format. N.B. This only indicates options handling, not any * other multi-destination options. * * - multi_servers : not implemented yet: indicates that the * remote storage module can handle multiple servers at backup * time. This should not be specified without multi_options. * multi_options without multi_servers is fine - it will just * cause only the first entry in the options array to be used. * * - config_templates : not implemented yet: indicates that * the remote storage module can output its configuration in * Handlebars format via the get_configuration_template() method. * * - conditional_logic : indicates that the remote storage module * can handle predefined logics regarding how backups should be * sent to the remote storage * * @return Array - an array of supported features (any features not * mentioned are assumed to not be supported) */ public function get_supported_features() { // The 'multi_options' options format is handled via only accessing options via $this->get_options() return array('multi_options', 'config_templates'); } public function delete($files, $method_obj = false, $sizeinfo = array()) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Unused variable is present because the function to perform delete for specific storage is not exist. $this->log('You do not have the UpdraftPlus '.$this->method.' add-on installed - get it from '.apply_filters("updraftplus_com_link", "https://updraftplus.com/shop/").''); $this->log(sprintf(__('You do not have the UpdraftPlus %s add-on installed - get it from %s', 'updraftplus'), $this->description, ''.apply_filters("updraftplus_com_link", "https://updraftplus.com/shop/").''), 'error', 'missingaddon-'.$this->method); return false; } public function listfiles($match = 'backup_') {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Unused variable is present because the function to perform listfiles for specific storage is not exist. return new WP_Error('no_addon', sprintf(__('You do not have the UpdraftPlus %s add-on installed - get it from %s', 'updraftplus'), $this->description, ''.apply_filters("updraftplus_com_link", "https://updraftplus.com/shop/"))); } /** * Get the configuration template * * @return String - the template, ready for substitutions to be carried out */ public function get_configuration_template() { ob_start(); ?> <tr class="{{css_class}} {{method_id}}"> <th>{{description}}:</th> <td>{{{image}}}{{addon_text}} - <a href="{{premium_url}}" target="_blank">{{premium_url_text}}</a></td> </tr> {{#unless php_version_supported}} <tr class="{{css_class}} {{method_id}}"> <th></th> <td> <em>{{error_msg_trans}} {{hosting_text}} {{php_version_text}}</em> </td> </tr> {{/unless}} <?php return ob_get_clean(); } /** * Retrieve a list of template properties by taking all the persistent variables and methods of the parent class and combining them with the ones that are unique to this module, also the necessary HTML element attributes and texts which are also unique only to this backup module * NOTE: Please sanitise all strings that are required to be shown as HTML content on the frontend side (i.e. wp_kses()), or any other technique to prevent XSS attacks that could come via WP hooks * * @return Array an associative array keyed by names that describe themselves as they are */ public function get_template_properties() { global $updraftplus; $properties = array( 'description' => $this->description, 'php_version_supported' => (bool) apply_filters('updraftplus_storage_meets_php_requirement', version_compare(phpversion(), $this->required_php, '>='), $this->method), 'image' => (!empty($this->image)) ? '<p><img src="'.UPDRAFTPLUS_URL.'/images/'.$this->image.'"></p>' : '', 'error_msg_trans' => $this->error_msg_trans, 'premium_url' => $updraftplus->get_url('premium'), 'premium_url_text' => __('follow this link to get it', 'updraftplus'), 'addon_text' => sprintf(__('%s support is available as an add-on', 'updraftplus'), $this->description), 'php_version_text' => sprintf(__('Your PHP version: %s.', 'updraftplus'), phpversion()), 'hosting_text' => __('You will need to ask your web hosting company to upgrade.', 'updraftplus'), ); return wp_parse_args($properties, $this->get_persistent_variables_and_methods()); } }
| ver. 1.4 |
Github
|
.
| PHP 8.0.30 | Generation time: 0 |
proxy
|
phpinfo
|
Settings