backup_methods)); $options = array(); $templates = array(); foreach ($storage_objects_and_ids as $method => $method_info) { $object = $method_info['object']; if (!$object->supports_feature('multi_options')) { ob_start(); do_action('updraftplus_config_print_before_storage', $method, null); $object->config_print(); $templates[$method] = ob_get_clean(); } else { $templates[$method] = $object->get_template(); } if (isset($method_info['instance_settings'])) { // Add the methods default settings so that we can add new instances $method_info['instance_settings']['default'] = $object->get_default_options(); foreach ($method_info['instance_settings'] as $instance_id => $instance_options) { $opts_without_transform = $instance_options; if ($object->supports_feature('multi_options')) { $opts_without_transform['instance_id'] = $instance_id; } $opts = $object->transform_options_for_template($opts_without_transform); foreach ($object->filter_frontend_settings_keys() as $filter_frontend_settings_key) { unset($opts[$filter_frontend_settings_key]); } $options[$method][$instance_id] = $opts; } } } return array( 'options' => $options, 'templates' => $templates, ); } /** * This method will return an array of remote storage objects and instance settings of the currently connected remote storage services. * * @param Array $services - an list of service identifiers (e.g. ['dropbox', 's3']) * * @uses self::get_storage_object() * * @return Array - returns an array, with a key equal to each member of the $services list passed in. The corresponding value is then an array with keys 'object', 'instance_settings'. The value for 'object' is an UpdraftPlus_BackupModule instance. The value for 'instance_settings' is an array keyed by associated instance IDs, with the values being the associated settings for the instance ID. */ public static function get_storage_objects_and_ids($services) { $storage_objects_and_ids = array(); // N.B. The $services can return any type of values (null, false, etc.) as mentioned from one of the comment found // in the "save_backup_to_history" function above especially if upgrading from (very) old versions. Thus, // here we're adding some check to make sure that we're receiving a non-empty array before iterating through // all the backup services that the user has in store. if (empty($services) || !is_array($services)) return $storage_objects_and_ids; foreach ($services as $method) { if ('none' === $method || '' == $method) continue; $remote_storage = self::get_storage_object($method); if (is_a($remote_storage, 'UpdraftPlus_BackupModule')) { if (!empty($method_objects[$method])) $storage_objects_and_ids[$method] = array(); $storage_objects_and_ids[$method]['object'] = $remote_storage; if ($remote_storage->supports_feature('multi_options')) { $settings_from_db = UpdraftPlus_Options::get_updraft_option('updraft_'.$method); $settings = is_array($settings_from_db) ? $settings_from_db : array(); if (!isset($settings['version'])) $settings = self::update_remote_storage_options_format($method); if (is_wp_error($settings)) { if (!empty($settings_from_db)) error_log("UpdraftPlus: failed to convert storage options format: $method"); $settings = array('settings' => array()); } if (empty($settings['settings'])) { // Try to recover by getting a default set of options for display if (is_callable(array($remote_storage, 'get_default_options'))) { $uuid = 's-'.md5(rand().uniqid().microtime(true)); $settings['settings'] = array($uuid => $remote_storage->get_default_options()); } // See: https://wordpress.org/support/topic/cannot-setup-connectionauthenticate-with-dropbox/ if (empty($settings['settings'])) { // This can get sent to the browser, and break the page, if the user has configured that. However, it should now (1.13.6+) be impossible for this condition to occur, now that we only log it after getting some default options. error_log("UpdraftPlus: Warning: settings for $method are empty. A dummy field is usually needed so that something is saved."); } } if (!empty($settings['settings'])) { if (!isset($storage_objects_and_ids[$method]['instance_settings'])) $storage_objects_and_ids[$method]['instance_settings'] = array(); foreach ($settings['settings'] as $instance_id => $storage_options) { $storage_objects_and_ids[$method]['instance_settings'][$instance_id] = $storage_options; } } } else { if (!isset($storage_objects_and_ids[$method]['instance_settings'])) $storage_objects_and_ids[$method]['instance_settings'] = $remote_storage->get_default_options(); } } else { error_log("UpdraftPlus: storage method not found: $method"); } } return $storage_objects_and_ids; } /** * This converts array-style options (i.e. late 2013-onwards) to * 2017-style multi-array-style options. * * N.B. Don't actually call this on any particular method's options * until the functions which read the options can cope! * * Don't call for settings that aren't array-style. You may lose * the settings if you do. * * It is safe to call this if you are not sure if the options are * already updated. * * @param String $method - the method identifier * * @returns Array|WP_Error - returns the new options, or a WP_Error if it failed */ public static function update_remote_storage_options_format($method) { global $updraftplus; // Prevent recursion static $already_active = false; if ($already_active) return new WP_Error('recursion', 'self::update_remote_storage_options_format() was called in a loop. This is usually caused by an options filter failing to correctly process a "recursion" error code'); if (!file_exists(UPDRAFTPLUS_DIR.'/methods/'.$method.'.php')) return new WP_Error('no_such_method', 'Remote storage method not found', $method); // Sanity/inconsistency check $settings_keys = $updraftplus->get_settings_keys(); $method_key = 'updraft_'.$method; if (!in_array($method_key, $settings_keys)) return new WP_Error('no_such_setting', 'Setting not found for this method', $method); $current_setting = UpdraftPlus_Options::get_updraft_option($method_key, array()); if ('' == $current_setting) $current_setting = array(); if (!is_array($current_setting) && false !== $current_setting) return new WP_Error('format_unrecognised', 'Settings format not recognised', array('method' => $method, 'current_setting' => $current_setting)); // Already converted? if (isset($current_setting['version'])) return $current_setting; if (empty($current_setting)) { $remote_storage = self::get_storage_object($method); $current_setting = $remote_storage->get_default_options(); } $new_setting = self::wrap_remote_storage_options($current_setting); $already_active = true; $updated = UpdraftPlus_Options::update_updraft_option($method_key, $new_setting); $already_active = false; if ($updated) { return $new_setting; } else { return new WP_Error('save_failed', 'Saving the options in the new format failed', array('method' => $method, 'current_setting' => $new_setting)); } } /** * This method will return an array of enabled remote storage objects and instance settings of the currently connected remote storage services. * * @param Array $services - an list of service identifiers (e.g. ['dropbox', 's3']) * * @uses self::get_storage_objects_and_ids() * * @return Array - returns an array, with a key equal to only enabled service member of the $services list passed in. The corresponding value is then an array with keys 'object', 'instance_settings'. The value for 'object' is an UpdraftPlus_BackupModule instance. The value for 'instance_settings' is an array keyed by associated enabled instance IDs, with the values being the associated settings for the enabled instance ID. */ public static function get_enabled_storage_objects_and_ids($services) { $storage_objects_and_ids = self::get_storage_objects_and_ids($services); foreach ($storage_objects_and_ids as $method => $method_information) { if (!$method_information['object']->supports_feature('multi_options')) continue; foreach ($method_information['instance_settings'] as $instance_id => $instance_information) { if (!isset($instance_information['instance_enabled'])) $instance_information['instance_enabled'] = 1; if (empty($instance_information['instance_enabled'])) { unset($storage_objects_and_ids[$method]['instance_settings'][$instance_id]); } } if (empty($storage_objects_and_ids[$method]['instance_settings'])) unset($storage_objects_and_ids[$method]); } return $storage_objects_and_ids; } /** * This method gets the remote storage information and objects and loops over each of them until we get a successful download of the passed in file. * * @param Array $services - a list of connected service identifiers (e.g. 'dropbox', 's3', etc.) * @param String $file - the name of the file * @param Integer $timestamp - the backup timestamp * @param Boolean $restore - a boolean to indicate if the caller of this method is a restore or not; if so, different messages are logged */ public static function get_remote_file($services, $file, $timestamp, $restore = false) { global $updraftplus; $fullpath = $updraftplus->backups_dir_location().'/'.$file; $storage_objects_and_ids = self::get_storage_objects_and_ids($services); $is_downloaded = false; $updraftplus->register_wp_http_option_hooks(); foreach ($services as $service) { if (empty($service) || 'none' == $service || $is_downloaded) continue; if ($restore) { $service_description = empty($updraftplus->backup_methods[$service]) ? $service : $updraftplus->backup_methods[$service]; $updraftplus->log(__("File is not locally present - needs retrieving from remote storage", 'updraftplus')." ($service_description)", 'notice-restore'); } $object = $storage_objects_and_ids[$service]['object']; if (!$object->supports_feature('multi_options')) { error_log("UpdraftPlus_Storage_Methods_Interface::get_remote_file(): Multi-options not supported by: ".$service); continue; } $instance_ids = $storage_objects_and_ids[$service]['instance_settings']; $backups_instance_ids = isset($backup_history[$timestamp]['service_instance_ids'][$service]) ? $backup_history[$timestamp]['service_instance_ids'][$service] : array(false); foreach ($backups_instance_ids as $instance_id) { if (isset($instance_ids[$instance_id])) { $options = $instance_ids[$instance_id]; } else { // If we didn't find a instance id match, it could be a new UpdraftPlus upgrade or a wipe settings with the same details entered so try the default options saved. $options = $object->get_options(); } $object->set_options($options, false, $instance_id); $download = self::download_file($file, $object); if (is_readable($fullpath) && false !== $download) { if ($restore) { $updraftplus->log(__('OK', 'updraftplus'), 'notice-restore'); } else { clearstatcache(); $updraftplus->log('Remote fetch was successful (file size: '.round(filesize($fullpath)/1024, 1).' KB)'); $is_downloaded = true; } break 2; } else { if ($restore) { $updraftplus->log(__('Error', 'updraftplus'), 'notice-restore'); } else { clearstatcache(); if (0 === @filesize($fullpath)) @unlink($fullpath); $updraftplus->log('Remote fetch failed'); } } } } $updraftplus->register_wp_http_option_hooks(false); } /** * Downloads a specified file into UD's directory * * @param String $file The basename of the file * @param UpdraftPlus_BackupModule $object The object of the service to use to download with. * * @return Boolean - Whether the operation succeeded. Inherited from the storage module's download() method. N.B. At the time of writing it looks like not all modules necessarily return true upon success; but false can be relied upon for detecting failure. */ private static function download_file($file, $object) { global $updraftplus; @set_time_limit(UPDRAFTPLUS_SET_TIME_LIMIT); $service = $object->get_id(); $updraftplus->log("Requested file from remote service: $service: $file"); if (method_exists($object, 'download')) { try { return $object->download($file); } catch (Exception $e) { $log_message = 'Exception ('.get_class($e).') occurred during download: '.$e->getMessage().' (Code: '.$e->getCode().', line '.$e->getLine().' in '.$e->getFile().')'; error_log($log_message); // @codingStandardsIgnoreLine if (function_exists('wp_debug_backtrace_summary')) $log_message .= ' Backtrace: '.wp_debug_backtrace_summary(); $updraftplus->log($log_message); $updraftplus->log(sprintf(__('A PHP exception (%s) has occurred: %s', 'updraftplus'), get_class($e), $e->getMessage()), 'error'); return false; // @codingStandardsIgnoreLine } catch (Error $e) { $log_message = 'PHP Fatal error ('.get_class($e).') has occurred during download. Error Message: '.$e->getMessage().' (Code: '.$e->getCode().', line '.$e->getLine().' in '.$e->getFile().')'; error_log($log_message); // @codingStandardsIgnoreLine if (function_exists('wp_debug_backtrace_summary')) $log_message .= ' Backtrace: '.wp_debug_backtrace_summary(); $updraftplus->log($log_message); $updraftplus->log(sprintf(__('A PHP fatal error (%s) has occurred: %s', 'updraftplus'), get_class($e), $e->getMessage()), 'error'); return false; } } else { $updraftplus->log("Automatic backup restoration is not available with the method: $service."); $updraftplus->log("$file: ".sprintf(__("The backup archive for this file could not be found. The remote storage method in use (%s) does not allow us to retrieve files. To perform any restoration using UpdraftPlus, you will need to obtain a copy of this file and place it inside UpdraftPlus's working folder", 'updraftplus'), $service)." (".UpdraftPlus_Manipulation_Functions::prune_updraft_dir_prefix($updraftplus->backups_dir_location()).")", 'error'); return false; } } /** * This method will update the old style remote storage options to the new style (Apr 2017) if the user has imported a old style version of settings * * @param Array $options - The remote storage options settings array * @return Array - The updated remote storage options settings array */ public static function wrap_remote_storage_options($options) { // Already converted? if (isset($options['version'])) return $options; // Generate an instance id $uuid = self::generate_instance_id(); $new_setting = array( 'version' => 1, ); if (!is_array($options)) $options = array(); $new_setting['settings'] = array($uuid => $options); return $new_setting; } /** * This method will return a random instance id string * * @return String - a random instance id */ private static function generate_instance_id() { // Cryptographic randomness not required. The prefix helps avoid potential for type-juggling issues. return 's-'.md5(rand().uniqid().microtime(true)); } }