client_id = defined('UPDRAFTPLUS_PCLOUD_CLIENT_ID') ? UPDRAFTPLUS_PCLOUD_CLIENT_ID : 'zrkDNwnlAGj'; $this->callback_url = defined('UPDRAFTPLUS_PCLOUD_CALLBACK_URL') ? UPDRAFTPLUS_PCLOUD_CALLBACK_URL : 'https://auth.updraftplus.com/auth/pcloud'; } /** * Supported features. * * @return Array */ public function get_supported_features() { // These 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( 'pclauth' => '', 'pcllocation' => '1', 'folderid' => 0, 'uploadid' => 0, 'folder' => '' ); } /** * 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['pclauth'])) { return true; } return false; } /** * Acts as a WordPress options filter * * @param Array $pcloud - An array of pCloud options. * * @return Array - the returned array can either be the set of updated pCloud settings or a WordPress error array */ public function options_filter($pcloud) { $opts = UpdraftPlus_Storage_Methods_Interface::update_remote_storage_options_format('pcloud'); 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: pCloud: $msg"); } // The saved options had a problem; so, return the new ones return $pcloud; } if (!is_array($pcloud)) return $opts; // Handle only version field being saved in the plugin free version. if (!isset($opts['settings'])) return $pcloud; // Remove instances that no longer exist foreach ($opts['settings'] as $instance_id => $storage_options) { if (!isset($pcloud['settings'][$instance_id])) unset($opts['settings'][$instance_id]); } if (empty($pcloud['settings'])) return $opts; foreach ($pcloud['settings'] as $instance_id => $storage_options) { // Now loop over the new options, and replace old options with them foreach ($storage_options as $key => $value) { // Reset "folderid" if folder name is changed if ('folder' == $key && isset($opts['settings'][$instance_id][$key]) && $opts['settings'][$instance_id][$key] != $value) $opts['settings'][$instance_id]['folderid'] = 0; 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; } } } return $opts; } /** * Proceed with the backup * * @param Array $backup_array - Array of files to be backed up. * * @return false|void|null */ public function backup($backup_array) { global $updraftplus; try { $pcloud = $this->bootstrap(); $info = $pcloud->account_info(); if (is_wp_error($info)) { $this->log('pCloud ('.$info->get_error_code().'): '.$info->get_error_message(), 'error'); $space_available = 0; } else { $space_available = $info['quota'] - $info['usedquota']; } } catch (Exception $e) { $this->log('Exception ('.get_class($e).') when trying to backup: ' . $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(); $opts = $this->get_options(); foreach ($backup_array as $file) { $hash = md5($file); $file_success = false; if (!file_exists($updraft_dir . '/' . $file)) { $file_success = true; } $filesize = filesize($updraft_dir . '/' . $file); $microtime = microtime(true); $pcl_upload_id = $this->jobdata_get('upload_pclid_' . $hash, 'None'); if ('None' === $pcl_upload_id) { $upload = $pcloud->create_upload(); if (is_wp_error($upload)) { $this->log('pCloud ('.$upload->get_error_code().'): '.$upload->get_error_message(), 'error'); return false; } $pcl_upload_id = $upload['uploadid']; $this->jobdata_set('upload_pclid_' . $hash, $pcl_upload_id); } else { $pcl_upload_id = intval($pcl_upload_id); } if ('None' !== $this->jobdata_get('upload_id_' . $hash, 'None')) { // Resume. $offset = $this->jobdata_get('upload_offset_' . $hash, 0); if ($offset) { $this->log("This is a resumption: $offset bytes had already been uploaded"); } $offset = intval($offset); } else { $offset = 0; } // We don't actually abort now - there's no harm in letting it try and then fail. if ($space_available < ($filesize - $offset)) { $this->log('File upload expected to fail: file data remaining to upload ($file) size is ' . (($filesize - $offset) / 1024) . ' Kb (overall file size; .' . $filesize . " b), whereas available quota is only $space_available b"); } $ufile = $file; $this->log("Attempt to upload: $file to: $ufile"); $upload_tick = microtime(true); $retries = 0; if (false === $file_success) { while (true) { $prev_offset = $offset; try { $new_offset = $pcloud->chunked_upload($updraft_dir . '/' . $file, $pcl_upload_id, $offset); if (is_wp_error($new_offset)) { throw new Exception($new_offset->get_error_message()); } $offset = $new_offset; if ($prev_offset === $offset) { // Failed, will retry. $retries++; } if (-2 === $offset) { // Success. $file_success = true; break; } $this->jobdata_set('upload_offset_' . $hash, $offset); } catch (Exception $e) { $this->log('chunked upload exception (' . get_class($e) . '): ' . $e->getMessage() . ' (line: ' . $e->getLine() . ', file: ' . $e->getFile() . ')'); if ($upload_tick > 0 && time() - $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(); $result = $pcloud->save($pcl_upload_id, $updraft_dir . '/' . $file, $opts['folderid']); if (is_wp_error($result)) $this->log('pCloud ('.$result->get_error_code().'): '.$result->get_error_message(), 'error'); die; } $retries++; } if (5 < $retries) { $this->log('chunked upload failed: too many failures.'); $this->log(__('Chunked upload failed', 'updraftplus'), 'error'); break; } } } if ($file_success) { $updraftplus->uploaded_file($file); $microtime_elapsed = microtime(true) - $microtime; $speedps = ($microtime_elapsed > 0) ? $filesize / $microtime_elapsed : 0; $speed = sprintf('%.2d', ($filesize / 1024)) . ' 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); $this->jobdata_delete('upload_pclid_' . $hash); $this->jobdata_delete('upload_offset_' . $hash); $result = $pcloud->save($pcl_upload_id, $updraft_dir . '/' . $file, $opts['folderid']); if (is_wp_error($result)) $this->log('pCloud ('.$result->get_error_code().'): '.$result->get_error_message(), 'error'); } } 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|WP_Error */ public function listfiles($match = 'backup_') { try { $opts = $this->get_options(); if (!$this->options_exist($opts)) return new WP_Error('no_settings', sprintf(__('No %s settings were found', 'updraftplus'), 'pCloud')); $pcloud = $this->bootstrap(); } catch (Exception $e) { $this->log($e->getMessage() . ' (line: ' . $e->getLine() . ', file: ' . $e->getFile() . ')'); $this->log(__('Listing the files failed:', 'updraftplus').' '.$e->getMessage(), 'warning'); return new WP_Error('listfiles', $e->getMessage()); } $results = array(); $backups = $pcloud->list_backups(); if (is_wp_error($backups)) { $this->log('pCloud ('.$backups->get_error_code().'): '.$backups->get_error_message(), 'error'); return $backups; } foreach ($backups as $backup) { $regex = str_replace('/', '\/', $match); if (empty($match) || preg_match('/' . $regex . '/', $backup['path'])) { $results[] = array( 'name' => $backup['name'], 'size' => $backup['size'], ); } } return $results; } /** * Delete files from the service using the pCloud API * * @param Array $files - array of filenames to delete. * * @return Boolean|String - either a boolean true or an error code string */ public function delete($files) { if (is_string($files)) { $files = array($files); } try { $pcloud = $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'), 'pCloud'), 'warning'); return 'service_unavailable'; } $any_failures = false; foreach ($files as $file) { $fullpath = '/' . $pcloud->get_backup_dir() . '/' . $file; $this->log("request deletion: $file"); $result = $pcloud->delete($fullpath); if (is_wp_error($result)) { $this->log('pCloud ('.$result->get_error_code().'): '.$result->get_error_message(), 'error'); } else { $file_success = 1; } if (!isset($file_success)) $any_failures = true; } return $any_failures ? 'file_delete_error' : true; } /** * Download method * * @param string $file File to be downloaded. * * @return false */ public function download($file) { global $updraftplus; $opts = $this->get_options(); $pclauth = !empty($opts['pclauth']) ? $opts['pclauth'] : ''; if (20 > strlen($pclauth)) { $this->log('You are not authenticated with pCloud'); $this->log(__('You are not authenticated with pCloud', 'updraftplus'), 'error'); return false; } try { $pcloud = $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 (!$pcloud) { return false; } $remote_files = $this->listfiles($file); foreach ($remote_files as $file_info) { if (basename($file_info['name']) === basename($file)) { $fname = basename($file_info['name']); return $updraftplus->chunked_download($fname, $this, $file_info['size'], apply_filters('updraftplus_pcloud_downloads_manually_break_up', false), null, 2 * 1048576); } } $this->log("$file: file not found in listing of remote directory"); return false; } /** * Callback used 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 bool - the data downloaded */ public function chunked_download($file, $headers, $data, $fh) { try { $pcloud = $this->bootstrap(); $needed_file = $pcloud->get_file_info(basename($file)); if (is_wp_error($needed_file)) { $this->log('pCloud ('.$needed_file->get_error_code().'): '.$needed_file->get_error_message(), 'error'); return false; } } 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'), 'pCloud'), 'warning'); return false; } if (count($needed_file) < 2 || !isset($needed_file['fileid'])) { $this->log('The requested file is no longer in the pCloud backup folder.'); return false; } $offset = 0; $retries = 0; while (true) { try { $offset = $pcloud->download($needed_file['fileid'], $fh, $headers, $offset); if (is_wp_error($offset)) throw new Exception($offset->get_error_message()); if ($offset >= ($needed_file['size'] - 1)) { fclose($fh); $get = true; break; } } catch (Exception $e) { $this->log($e); $this->log($e->getMessage(), 'error'); $get = false; $retries++; if (40 < $retries) { break; } // Sometimes the server can not deliver the file content for some many reasons, we can wait a little and try again. sleep(2); } } return $get; } /** * Get the pre configuration template * * @return String - the template */ public function get_pre_configuration_template() { ?> {{storage_image_title}}

{{{storage_long_description}}}

: {{folder_path}} {{authentication_label}}: {{#if is_authenticated}}

{{already_authenticated_label}} {{deauthentication_link_text}}

{{/if}} {{#if ownername_sentence}}
{{ownername_sentence}} {{/if}}

{{{authentication_link_text}}}

UPDRAFTPLUS_URL.'/images/pcloud-logo.png', 'storage_image_title' => __(sprintf(__('%s logo', 'updraftplus'), $updraftplus->backup_methods[$this->get_id()])), 'storage_long_description' => wp_kses(sprintf(__('Please read %s for use of our %s authorization app (none of your backup data is sent to us).', 'updraftplus'), ''.__('this privacy policy', 'updraftplus').'', $updraftplus->backup_methods[$this->get_id()]), $this->allowed_html_for_content_sanitisation()), 'authentication_label' => sprintf(__('Authenticate with %s', 'updraftplus'), $updraftplus->backup_methods[$this->get_id()]), 'already_authenticated_label' => __('(You are already authenticated).', 'updraftplus'), 'deauthentication_link_text' => sprintf(__("Follow this link to remove these settings for %s.", 'updraftplus'), $updraftplus->backup_methods[$this->get_id()]), 'authentication_link_text' => wp_kses(sprintf(__("After 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_nonce' => wp_create_nonce($this->get_id().'_deauth_nonce'), ); 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['pclauth'])) { $opts['ownername'] = empty($opts['ownername']) ? '' : $opts['ownername']; if ($opts['ownername']) { $opts['ownername_sentence'] = sprintf(__("Account holder's name: %s.", 'updraftplus'), $opts['ownername']).' '; } $opts['is_authenticated'] = true; } $opts['folder_path'] = apply_filters('updraftplus_pcloud_backup_dir', 'UpdraftPlus').'/'; $opts = apply_filters("updraftplus_options_pcloud_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( 'ownername', 'pclauth', ); } /** * Over-rides the parent to allow this method to output extra information about using the correct account for OAuth authentication * * @return false */ public function output_account_warning() { return false; } /** * Handles various URL actions, as indicated by the updraftplus_pcloudauth URL parameter * * @return null */ public function action_auth() { if (isset($_GET['updraftplus_pcloudauth'])) { if ('doit' == stripslashes($_GET['updraftplus_pcloudauth'])) { $this->action_authenticate_storage(); return; } elseif ('deauth' == stripslashes($_GET['updraftplus_pcloudauth'])) { $this->action_deauthenticate_storage(); return; } } elseif (isset($_REQUEST['state'])) { $parts = explode(':', stripslashes($_GET['state'])); $state = $parts[0]; if ('success' == $state) { $raw_state = stripslashes($_GET['state']); if (isset($_GET['code'])) $raw_code = urldecode(stripslashes($_GET['code'])); $this->do_complete_authentication($raw_state, $raw_code); } } } /** * Acquire single-use authorization code from pCloud 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(); // 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; $token = 'token'.$prefixed_instance_id.UpdraftPlus_Options::admin_page_url().'?action=updraftmethod-pcloud-auth'; $params = array( 'response_type' => 'code', 'client_id' => $this->client_id, 'redirect_uri' => $this->callback_url, 'state' => $token, 'access_type' => 'offline', 'force_reapprove' => 'true', 'returnqueryparams' => 1 ); if (headers_sent()) { $this->log(sprintf(__('The %s authentication could not go ahead, because something else on your site is breaking it.', 'updraftplus'), 'pCloud').' '.__('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://my.pcloud.com/oauth2/authorize?'.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) { $code = json_decode(base64_decode($code), true); if (!is_bool($code) && isset($code['access_token']) && 30 < strlen($code['access_token']) && isset($code['locationid'])) { $opts = $this->get_options(); $opts['pclauth'] = $code['access_token']; $opts['pcllocation'] = $code['locationid']; // 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')); } } /** * This method will setup the authenticated admin notice, 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; try { $pcloud = $this->bootstrap(); $info = $pcloud->account_info(); if (is_wp_error($info)) { $this->log('pCloud ('.$info->get_error_code().'): '.$info->get_error_message(), 'error'); } } catch (Exception $e) { $accountinfo_err = sprintf(__("%s error: %s", 'updraftplus'), 'pCloud', $e->getMessage()).' ('.$e->getCode().')'; $this->log('pCloud error: ' . $e->getMessage() . ' (line: ' . $e->getLine() . ', file: ' . $e->getFile() . ')'); $this->log(sprintf(__('error: %s (see log file for more)', 'updraftplus'), $e->getMessage()), 'error'); } $message = "".__('Success:', 'updraftplus').' '.sprintf(__('you have authenticated your %s account', 'updraftplus'), 'pCloud'); if (isset($info['quota']) || isset($info['usedquota']) || isset($info['email'])) { $available_quota = $info['quota'] - $info['usedquota']; $used_perc = round($info['usedquota']*100/$info['quota'], 1); $opts = $this->get_options(); $opts['ownername'] = $info['email']; $this->set_options($opts, true); $message .= ".
".sprintf(__('Your %s account name: %s', 'updraftplus'), 'pCloud', htmlspecialchars($info['email'])); $message .= '
'.sprintf(__('Your %s quota usage: %s %% used, %s available', 'updraftplus'), 'pCloud', $used_perc, round($available_quota/1048576, 1).' MB'); } else { $message .= " (".__('though part of the returned information was not as expected - whether this indicates a real problem cannot be determined', 'updraftplus').")"; if (!empty($accountinfo_err)) $message .= "
".htmlspecialchars($accountinfo_err); } if ($return_instead_of_echo) { return "

{$message}

"; } else { $updraftplus_admin->show_admin_warning($message); } } /** * This basically reproduces the relevant bits of bootstrap.php from the SDK * * @return object * @throws Exception Throws standard exception. */ public function bootstrap() { if (!class_exists('UpdraftPlus_Pcloud_API')) { include_once UPDRAFTPLUS_DIR . '/includes/pcloud/UpdraftPlus_Pcloud_API.php'; } // if (false === $opts) $opts = $this->options; // $opts = $this->get_options(); $storage = $this->get_storage(); if (!empty($storage) && !is_wp_error($storage)) { return $storage; } $opts = $this->get_options(); $pclauth = !empty($opts['pclauth']) ? $opts['pclauth'] : ''; $pcllocation = !empty($opts['pcllocation']) ? intval($opts['pcllocation']) : ''; if (empty($pclauth) || 20 > strlen($pclauth)) { throw new Exception('You are not logged in.'); } $storage = new UpdraftPlus_Pcloud_API(); $storage->set_auth($pclauth); $storage->set_location($pcllocation); $storage->set_folder($opts['folder']); if (empty($opts['folderid'])) { $folder_id = $storage->get_upload_dir_id(); if (is_wp_error($folder_id)) $this->log('pCloud ('.$folder_id->get_error_code().'): '.$folder_id->get_error_message(), 'error'); if (is_numeric($folder_id) && 0 < $folder_id) { $opts['folderid'] = $folder_id; } $this->set_options($opts, true); } else { $folder_id = intval($opts['folderid']); } if (empty($folder_id) || 10 > $folder_id) $this->log('pCloud error: Failed to get folder id; backup will be uploaded to the root directory'); $this->set_storage($storage); return $storage; } }