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')) require_once(UPDRAFTPLUS_DIR.'/methods/backup-module.php');
class UpdraftPlus_BackupModule_googledrive extends UpdraftPlus_BackupModule {
private $ids_from_paths = array();
private $multi_directories = array();
private $registered_prune = false;
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';
public function action_auth() {
if (isset($_GET['state'])) {
$parts = explode(':', $_GET['state']);
if ('success' == $state) {
if (isset($_GET['user_id']) && isset($_GET['access_token'])) {
'user_id' => $_GET['user_id'],
'access_token' => $_GET['access_token']
$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.
* Check whether options have been set up by the user, or not
* @param Array $opts - the potential options
public function options_exist($opts) {
if (is_array($opts) && (!empty($opts['user_id']) || !empty($opts['token']))) return true;
* Get the Google folder ID for the root of the drive
private function root_id() {
if (empty($this->root_id)) $this->root_id = $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();
while ('/' == substr($path, 0, 1)) {
$path = substr($path, 1);
$cache_key = empty($path) ? '/' : ($one_only ? $path : 'multi:'.$path);
if (isset($this->ids_from_paths[$cache_key])) return $this->ids_from_paths[$cache_key];
$current_parent_id = $this->root_id();
$nodes = explode('/', $path);
foreach ($nodes as $element) {
$sub_items = $this->get_subitems($current_parent_id, 'dir', $element);
foreach ($sub_items as $item) {
if ($item->getTitle() == $element) {
$current_path .= $element.'/';
$current_parent_id = $item->getId();
$found[$current_parent_id] = strtotime($item->getCreatedDate());
$this->log("id_from_path: exception: ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')');
$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(
array('mimeType' => 'application/vnd.google-apps.folder')
$current_path .= $element.'/';
$current_parent_id = $dir->getId();
if (empty($this->ids_from_paths)) $this->ids_from_paths = array();
$this->ids_from_paths[$cache_key] = ($one_only || empty($found) || 1 == count($found)) ? $current_parent_id : $found;
return $this->ids_from_paths[$cache_key];
$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.");
$this->log("id_from_path: retry ($retry_count)");
$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);
* Runs upon the WP action updraftplus_prune_retained_backups_finished
public function prune_retained_backups_finished() {
if (empty($this->multi_directories) || count($this->multi_directories) < 2) return;
$storage = $this->bootstrap();
if (false == $storage || is_wp_error($storage)) return;
foreach (array_keys($this->multi_directories) as $drive_id) {
if (!isset($oldest_reference)) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
$oldest_reference = new UDP_Google_Service_Drive_ParentReference;
$oldest_reference->setId($oldest_id);
// All found files should be moved to the oldest folder
$sub_items = $this->get_subitems($drive_id, 'file');
$this->log('list files: failed to access chosen folder: '.$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')');
foreach ($sub_items as $item) {
$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);
$this->log("move: exception: ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')');
if (!empty($sub_items)) {
$sub_items = $this->get_subitems($drive_id, 'file');
$this->log('list files: failed to access chosen folder: '.$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')');
$storage->files->delete($drive_id);
$this->log("removed empty duplicate folder ($drive_id)");
$this->log("delete empty duplicate folder ($drive_id): exception: ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')');
* 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
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'])) {
$parent = is_array($opts['parentid']) ? $opts['parentid']['id'] : $opts['parentid'];
$parent = $this->id_from_path('UpdraftPlus', $one_only);
return empty($parent) ? $this->root_id() : $parent;
public function listfiles($match = 'backup_') {
$opts = $this->get_options();
$use_master = $this->use_master($opts);
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')));
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;
$parent_id = $this->get_parent_id($opts);
$sub_items = $this->get_subitems($parent_id, 'file');
return new WP_Error(__('Google Drive list files: failed to access parent folder', 'updraftplus').": ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')');
foreach ($sub_items as $item) {
$title = $item->getTitle();
if (0 === strpos($title, $match)) {
$results[] = array('name' => $title, 'size' => $item->getFileSize());
$this->log("list: exception: ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')');
* 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
private function access_token($refresh_token, $client_id, $client_secret) {
$this->log("requesting access token: client_id=$client_id");
'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',
if (is_wp_error($result)) {
$this->log("error when requesting access token");
foreach ($result->get_error_messages() as $msg) $this->log("Error message: $msg");
$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'];
$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. 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'])));
* 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) {
return $this->callback_url;
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
$client_id = $this->client_id;
$token = 'token'.$prefixed_instance_id.$this->redirect_uri();
$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.
'response_type' => 'code',
'client_id' => $client_id,
'redirect_uri' => $this->redirect_uri($use_master),
'scope' => apply_filters('updraft_googledrive_scope', 'https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/drive.readonly https://www.googleapis.com/auth/userinfo.profile'),
'access_type' => 'offline',
'approval_prompt' => 'force'
$this->log(sprintf(__('The %s authentication could not go ahead, because something else on your site is breaking it. Try disabling your other plugins and switching to a default theme. (Specifically, you are looking for the component that sends output (most likely PHP warnings/errors) before the page begins. Turning off any debugging settings may also help).', ''), 'Google Drive'), 'error');
header('Location: https://accounts.google.com/o/oauth2/auth?'.http_build_query($params, null, '&'));
* 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']);
// 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);
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']);
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'])) {
'client_id' => $opts['clientid'],