Changeset 558116
- Timestamp:
- 06/14/2012 07:38:44 PM (13 years ago)
- Location:
- backup/trunk
- Files:
-
- 5 added
- 3 edited
Legend:
- Unmodified
- Added
- Removed
-
backup/trunk/backup.php
r549327 r558116 2 2 /* 3 3 Plugin Name: Backup 4 Version: 1.1.64 Version: 2.0 5 5 Plugin URI: http://hel.io/wordpress/backup/ 6 6 Description: Backup your WordPress website to Google Drive. 7 7 Author: Sorin Iclanzan 8 8 Author URI: http://hel.io/ 9 License: GPL3 9 10 */ 10 11 11 // This is run when you activate the plugin, checking for compatibility, adding the default options to the database 12 register_activation_hook(__FILE__,'backup_activation'); 13 function backup_activation() { 14 // Check for compatibility 15 try { 16 // try to create backup folder inside wp-contents 17 if ( !is_dir( WP_CONTENT_DIR . '/backup' ) ) 18 if ( ! @mkdir( WP_CONTENT_DIR . '/backup', 0755 ) ) 19 throw new Exception(__('Could not create \'backup\' folder inside \'wp-content\'. Either create it yourself or set the right permissions and try activating the plugin again.', 'backup')); 20 // try to create log file 21 if ( FALSE === file_put_contents( WP_CONTENT_DIR . '/backup/backup.log', "#Fields:\tdate\ttime\ttype\tmessage\tfile\tline\n" ) ) 22 throw new Exception(__('Could not create log file. Make sure the web server has the right permissions to write to the \'backup\' folder.', 'backup')); 23 24 // check allow_url_fopen 25 if(!ini_get('allow_url_fopen')) { 26 throw new Exception(__('Please enable \'allow_url_fopen\' in PHP. Backup can not function without it.', 'backup')); 27 } 28 29 // check zip extension 30 if(!extension_loaded('zip')) { 31 throw new Exception(__('Please load the \'zip\' PHP extension. It is needed in order to create the archive to be backed up.', 'backup')); 32 } 33 34 // check file_get_contents to docs.google.com 35 $context = array( 36 'http' => array( 37 'timeout' => 3, 12 /* Copyright 2012 Sorin Iclanzan (email : [email protected]) 13 14 This file is part of Backup. 15 16 Backup is free software: you can redistribute it and/or modify 17 it under the terms of the GNU General Public License as published by 18 the Free Software Foundation, either version 3 of the License, or 19 (at your option) any later version. 20 21 Backup is distributed in the hope that it will be useful, 22 but WITHOUT ANY WARRANTY; without even the implied warranty of 23 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 24 GNU General Public License for more details. 25 26 You should have received a copy of the GNU General Public License 27 along with Foobar. If not, see http://www.gnu.org/licenses/gpl.html. 28 */ 29 30 // Only load the plugin if needed. 31 if ( is_admin() || defined('DOING_CRON') || isset($_GET['doing_wp_cron']) || isset($_GET['backup']) || isset($_GET['resume_backup']) ) { 32 33 // Load required classes. 34 if ( ! class_exists('GOAuth') ) 35 require_once('class-goauth.php'); 36 if ( ! class_exists('GDocs') ) 37 require_once('class-gdocs.php'); 38 39 // Load helper functions 40 require_once('functions.php'); 41 42 /** 43 * Backup for WordPress class. 44 * 45 * Implements backup functionality in WordPress. Currenly supports 46 * backing up on the local filesystem and on Google Drive. 47 * 48 * @uses WP_Error for storing error messages. 49 * @uses GOAuth for Google OAuth2 authorization. 50 * @uses GData to upload backups to Google Drive (Docs). 51 */ 52 class Backup { 53 54 /** 55 * Stores the plugin base filesystem directory 56 * 57 * @var string 58 * @access private 59 */ 60 private $plugin_dir; 61 62 /** 63 * Stores the unique text domain used for I18n 64 * 65 * @var string 66 * @access private 67 */ 68 private $text_domain; 69 70 /** 71 * Stores plugin options. 72 * 73 * Options are automatically updated in the database when the destructor is called. 74 * 75 * @var array 76 * @access private 77 */ 78 private $options; 79 80 /** 81 * Stores custom schedule intervals to use with WP_Cron. 82 * 83 * @var array 84 * @access private 85 */ 86 private $schedules; 87 88 /** 89 * Stores the redirect URI needed by GOAuth. 90 * 91 * @var string 92 * @access private 93 */ 94 private $redirect_uri; 95 96 /** 97 * Stores an instance of GDocs. 98 * 99 * @var GDocs 100 * @access private 101 */ 102 private $docs; 103 104 /** 105 * Stores an instance of GOAuth. 106 * 107 * @var GOAuth 108 * @access private 109 */ 110 private $goauth; 111 112 /** 113 * Stores messages that need to be displayed on the option page. 114 * 115 * @var array 116 * @access private 117 */ 118 private $messages = array(); 119 120 /** 121 * Stores the absolute path to the directory this plugin will use to store files. 122 * 123 * @var string 124 * @access private 125 */ 126 private $local_folder; 127 128 /** 129 * Stores the absolute path and file name where database dumps are saved. 130 * 131 * @var string 132 * @access private 133 */ 134 private $dump_file; 135 136 /** 137 * Stores the absolute path to the log file. 138 * 139 * @var string 140 * @access private 141 */ 142 private $log_file; 143 144 /** 145 * Stores the log file basename 146 * 147 * @var string 148 * @access private 149 */ 150 private $log_filename; 151 152 /** 153 * Stores a list of paths to directories and files that are available for backup. 154 * 155 * @var array 156 * @access private 157 */ 158 private $sources; 159 160 /** 161 * Stores paths that are to be excluded when backing up. 162 * 163 * @var array 164 * @access private 165 */ 166 private $exclude = array(); 167 168 /** 169 * Stores a list of URIs representing the scope required by GOAuth. 170 * 171 * @var array 172 * @access private 173 */ 174 private $scope; 175 176 /** 177 * Stores the timestamp at the time of the execution. 178 * 179 * @var integer 180 * @access private 181 */ 182 private $time; 183 184 /** 185 * Stores the identifier of the plugin options page. 186 * 187 * @var string 188 * @access private 189 */ 190 private $pagehook; 191 192 /** 193 * Stores the ID of the current user 194 * 195 * @var integer 196 * @access private 197 */ 198 private $user_id; 199 200 function __construct() { 201 $this->time = current_time('timestamp'); 202 $this->plugin_dir = dirname(plugin_basename(__FILE__)); 203 $this->text_domain = 'backup'; 204 $this->local_folder = WP_CONTENT_DIR . '/backup'; 205 206 // Enable internationalization 207 load_plugin_textdomain($this->text_domain, false, $this->plugin_dir . '/languages' ); 208 209 $this->goauth_scope = array( 210 'https://www.googleapis.com/auth/drive.file', 211 'https://docs.google.com/feeds/', 212 'https://docs.googleusercontent.com/', 213 'https://spreadsheets.google.com/feeds/' 214 ); 215 216 $this->schedules = array( 217 'weekly' => array( 218 'interval' => 604800, 219 'display' => __('Weekly', $this->text_domain) 220 ), 221 'montly' => array( 222 'interval' => 2592000, 223 'display' => __('Monthly', $this->text_domain) 38 224 ) 39 225 ); 40 $result = @file_get_contents('http://docs.google.com/', false, stream_context_create($context)); 41 if($result === false) { 42 throw new Exception(__('Cannot connect to Google Drive. Backup could not be activated.', 'backup')); 43 } 44 45 // check OpenSSL 46 if(!function_exists('openssl_open')) { 47 throw new Exception(__('Please enable OpenSSL in PHP. Backup needs it to communicate with Google Drive.', 'backup')); 48 } 49 50 // check SimpleXMLElement 51 if(!class_exists('SimpleXMLElement')) { 52 throw new Exception(__('Please enable SimpleXMLElement in PHP. Backup could not be activated.', 'backup')); 53 } 54 } 55 catch(Exception $e) { 56 $plugin_basename = dirname(plugin_basename(__FILE__)); 57 deactivate_plugins($plugin_basename.'/backup.php', true); 58 echo '<div id="message" class="error">' . $e->getMessage() . '</div>'; 59 trigger_error('Could not activate Backup.', E_USER_ERROR); 60 } 61 62 // Default options 63 $backup_options = array( 64 'token' => '', 65 'folder' => '', 66 'frequency' => 'never', 67 'client_id' => '', 68 'client_secret' => '', 69 'last_backup' => '', 70 'local_number' => 10, 71 'drive_number' => 10, 72 'local_files' => array(), 73 'drive_files' => array(), 74 ); 75 76 // Add options 77 update_option( 'backup_options', $backup_options ); 78 79 } 80 81 /** 82 * Backup Deactivation 83 * 84 * This function is called whenever the plugin is being deactivated and removes 85 * all files and directories it created as well as the options stored in the database. 86 * It also revokes access to the Google Account associated with it and removes all scheduled events created. 87 */ 88 function backup_deactivation() { 89 $options = get_option( 'backup_options' ); 90 if ( ! empty( $options['token'] ) ) 91 @file_get_contents( 'https://accounts.google.com/o/oauth2/revoke?token=' . $options['token'] ); 92 delete_option( 'backup_options' ); 93 if ( wp_next_scheduled( 'backup_schedule' ) ) { 94 wp_clear_scheduled_hook('backup_schedule'); 95 } 96 rrmdir( WP_CONTENT_DIR . '/backup' ); 97 if ( file_exists( WP_CONTENT_DIR . '/backup.sql' ) ) 98 unlink( WP_CONTENT_DIR . '/backup.sql' ); 99 } 100 register_deactivation_hook( __FILE__, 'backup_deactivation' ); 101 102 // Add custom schedule intervals 103 add_filter( 'cron_schedules', 'cron_add_intervals' ); 104 function cron_add_intervals( $schedules ) { 105 $schedules['weekly'] = array( 106 'interval' => 604800, 107 'display' => __( 'Weekly', 'backup' ) 108 ); 109 $schedules['monthly'] = array( 110 'interval' => 2592000, 111 'display' => __( 'Monthly', 'backup' ) 112 ); 113 return $schedules; 114 } 115 116 // Add options page in the admin menu 117 add_action('admin_menu','backup_menu'); 118 function backup_menu() { 119 add_options_page('Backup Settings', 'Backup', 'manage_options', 'backup', 'backup_options_page'); 120 } 121 122 // Add "Settings" link to the plugins page 123 add_filter( 'plugin_action_links', 'backup_action_links',10,2); 124 function backup_action_links( $links, $file ) { 125 if ( $file != plugin_basename( __FILE__ )) 126 return $links; 127 128 $settings_link = '<a href="options-general.php?page=backup">Settings</a>'; 129 130 array_unshift( $links, $settings_link ); 131 132 return $links; 133 } 134 135 // Display options page 136 function backup_options_page() { 137 // Display messages and errors 138 if ( isset( $_GET['message'] ) ) 139 echo '<div id="message" class="updated fade"> 140 <p>' . $_GET['message'] . '</p> 141 </div>'; 142 if ( isset( $_GET['error'] ) ) 143 echo '<div id="message" class="error"> 144 <p>' . $_GET['error'] . '</p> 145 </div>'; 146 147 // Check to see if we have authorization; if we have a token saved it means we have authorization. 148 $options = get_option('backup_options'); 149 ?> 150 <div class="wrap"> 151 <div id="icon-options-general" class="icon32"><br/></div><h2><?php _e( 'Backup Settings', 'backup' ); ?></h2> 152 <div id="poststuff" class="metabox-holder has-right-sidebar"> 153 <div class="inner-sidebar"> 154 <div id="submitdiv" class="postbox"> 155 <h3><?php _e( 'Status', 'backup' ); ?></h3> 156 <div class="inside"> 157 <div class="misc-pub-section"><?php _e( 'Most recent backup:', 'backup' ); ?><br/><?php 158 if ( $options['last_backup'] ) { 159 echo '<strong>' . date( __( "M j, Y \a\\t H:i:s", 'backup' ), $options['last_backup'] ) . '</strong>'; 160 if ( $options['local_number'] > 0 ) { 161 ?><br/><br/><a class="button-secondary" title="<?php _e( 'Download most recent backup', 'backup'); ?>" href="<?php echo WP_CONTENT_URL . substr( end( $options['local_files'] ), strlen( WP_CONTENT_DIR ) ); ?>">Download backup</a><?php 162 } 163 } 164 else { 165 echo '<strong>' . __( 'never', 'backup' ) . '</strong>'; 166 } 167 ?></div> 168 <div class="misc-pub-section"><?php _e( 'Next scheduled backup:', 'backup' ); ?><br/><strong><?php 169 if ( $next = wp_next_scheduled( 'backup_schedule' ) ) 170 echo date( __( "M j, Y \a\\t H:i:s", 'backup' ), $next ); 171 else 172 _e( 'never', 'backup' ); 173 ?></strong></div> 174 <div class="misc-pub-section misc-pub-section-last"><?php _e( 'Manual backup URL:', 'backup' ); ?><br/><kbd><?php echo home_url( '/backup/' ); ?></kbd></div> 175 </div> 176 </div> 177 </div> 178 </div> 179 <h3><?php _e( 'Authorization', 'backup' ); ?></h3> 180 <?php if ( $options['token'] == '' ) { ?> 181 <form action="options-general.php?page=backup&action=auth" method="post"> 182 <p><?php _e( 'Before we can do anything you need to give this plugin authorization to use Google Dirve.', 'backup' ); ?></p> 183 <p><?php printf( __( 'You can create a Client ID in the API Access section of your %s if you don\'t have one. A client secret will also be generated for you as long as you select \'Web Application\' as the application type.', 'backup' ), '<a href="https://code.google.com/apis/console/" target="_blank">Google API Console</a>' ); ?></p> 184 <p><?php printf( __( 'Make sure to add %s as the authorized redirect URI when asked.', 'backup' ), '<kbd>' . admin_url( 'options-general.php?page=backup&action=auth' ) . '</kbd>' ); ?></p> 185 <table class="form-table"> 186 <tbody> 187 <tr valign="top"> 188 <th scope="row"><?php _e( 'Client ID', 'backup' ); ?></th> 189 <td><input name='client_id' type='text' class='regular-text' value='<?php echo $options['client_id']; ?>' /></td> 190 </tr> 191 <tr valign="top"> 192 <th scope="row"><?php _e( 'Client secret', 'backup' ); ?></th> 193 <td><input name='client_secret' type='text' class='regular-text' value='<?php echo $options['client_secret']; ?>' /></td> 194 </tr> 195 </tbody> 196 </table> 197 <p class="submit"> 198 <input name="authorize" type="submit" class="button-secondary" value="<?php _e('Authorize', 'backup') ?>" /> 226 $this->redirect_uri = admin_url('options-general.php?page=backup&action=auth'); 227 228 // Get options if they exist, else set default 229 if ( ! $this->options = get_option('backup_options') ) { 230 $this->options = array( 231 'refresh_token' => '', 232 'local_folder' => relative_path(ABSPATH, $this->local_folder), 233 'drive_folder' => '', 234 'backup_frequency' => 'never', 235 'source_list' => array( 'database', 'content', 'uploads', 'plugins' ), 236 'exclude_list' => array( '.svn', '.git', '.DS_Store' ), 237 'client_id' => '', 238 'client_secret' => '', 239 'last_backup' => '', 240 'local_number' => 10, 241 'drive_number' => 10, 242 'local_files' => array(), 243 'drive_files' => array(), 244 'quota_total' => '', 245 'quota_used' => '', 246 'chunk_size' => 0.5, // MiB 247 'time_limit' => 120 // seconds 248 ); 249 } 250 251 $this->local_folder = absolute_path($this->options['local_folder'], ABSPATH); 252 $this->dump_file = $this->local_folder . '/dump.sql'; 253 $upload_dir = wp_upload_dir(); 254 255 $this->sources = array( 256 'database' => array( 'title' => __('Database', $this->text_domain), 'path' => $this->dump_file ), 257 'content' => array( 'title' => __('Content', $this->text_domain), 'path' => WP_CONTENT_DIR ), 258 'uploads' => array( 'title' => __('Uploads', $this->text_domain), 'path' => $upload_dir['basedir'] ), 259 'plugins' => array( 'title' => __('Plugins', $this->text_domain), 'path' => WP_PLUGIN_DIR ), 260 'wordpress' => array( 'title' => __('WordPress', $this->text_domain), 'path' => ABSPATH ) 261 ); 262 263 $this->log_filename = 'backup.log'; 264 $this->log_file = $this->local_folder . '/' . $this->log_filename; 265 $this->exclude[] = $this->local_folder; 266 267 register_activation_hook(__FILE__, array(&$this, 'activate')); 268 register_deactivation_hook(__FILE__, array(&$this, 'deactivate')); 269 270 // Add custom cron intervals 271 add_filter('cron_schedules', array(&$this, 'cron_add_intervals')); 272 273 // Set the screen layout to use 2 columns 274 add_filter('screen_layout_columns', array(&$this, 'on_screen_layout_columns'), 10, 2); 275 276 // Link to the settings page from the plugins pange 277 add_filter('plugin_action_links', array(&$this, 'action_links'), 10, 2); 278 279 // Add 'Backup' to the Settings admin menu; save default metabox layout in the database 280 add_action('admin_menu', array(&$this, 'backup_menu')); 281 282 // Handle Google OAuth2 283 if ( $this->is_auth() ) 284 add_action('init', array(&$this, 'auth')); 285 286 // Enable manual backup URI 287 add_action('template_redirect', array(&$this, 'manual_backup')); 288 289 // Do backup on schedule 290 add_action('backup_schedule', array(&$this, 'do_backup')); 291 292 // Resume backup on schedule 293 add_action('backup_resume', array(&$this, 'backup_resume')); 294 295 $this->goauth = new GOAuth($this->options['client_id'], $this->options['client_secret'], $this->redirect_uri, $this->options['refresh_token']); 296 297 } 298 299 /** 300 * This is run when you activate the plugin, checking for compatibility, adding the default options to the database. 301 */ 302 public function activate() { 303 // Check for compatibility 304 try { 305 // check OpenSSL 306 if(!function_exists('openssl_open')) { 307 throw new Exception(__('Please enable OpenSSL in PHP. Backup needs it to communicate with Google Drive.', $this->text_domain)); 308 } 309 310 // check SimpleXMLElement 311 if(!class_exists('SimpleXMLElement')) { 312 throw new Exception(__('Please enable SimpleXMLElement in PHP. Backup could not be activated.', $this->text_domain)); 313 } 314 } 315 catch(Exception $e) { 316 deactivate_plugins($plugin_dir . '/backup.php', true); 317 echo '<div id="message" class="error">' . $e->getMessage() . '</div>'; 318 trigger_error('Could not activate Backup.', E_USER_ERROR); 319 return; 320 } 321 322 // Add the default options to the database, without letting WP autoload them 323 add_option('backup_options', $this->options, '', 'no'); 324 325 // We call this here just to get the page hook 326 $this->pagehook = add_options_page('Backup Settings', 'Backup', 'manage_options', 'backup', array(&$this, 'options_page')); 327 328 if ( ! $this->user_id ) 329 $this->user_id = get_current_user_id(); 330 331 // Set the default order of the metaboxes. 332 if ( ! get_user_meta($this->user_id, "meta-box-order_".$this->pagehook, true) ) { 333 $meta_value = array( 334 'side' => 'metabox-authorization,metabox-status', 335 'normal' => 'metabox-advanced', 336 'advanced' => 'metabox-logfile', 337 ); 338 update_user_meta($this->user_id, "meta-box-order_".$this->pagehook, $meta_value); 339 } 340 341 // Set the default closed metaboxes. 342 if ( ! get_user_meta($this->user_id, "closedpostboxes_".$this->pagehook, true) ) { 343 $meta_value = array('metabox-advanced'); 344 update_user_meta($this->user_id, "closedpostboxes_".$this->pagehook, $meta_value); 345 } 346 347 // Set the default hidden metaboxes. 348 if ( ! get_user_meta($this->user_id, "metaboxhidden_".$this->pagehook, true) ) { 349 $meta_value = array('metabox-logfile'); 350 update_user_meta($this->user_id, "metaboxhidden_".$this->pagehook, $meta_value); 351 } 352 353 // Set the default number of columns. 354 if ( ! get_user_meta($this->user_id, "screen_layout_".$this->pagehook, true) ) { 355 update_user_meta($this->user_id, "screen_layout_".$this->pagehook, 2); 356 } 357 358 // try to create the default backup folder and backup log 359 if ( !@is_dir($this->local_folder) ) 360 if ( wp_mkdir_p($this->local_folder) ) 361 if ( !@is_file($this->log_file) ) 362 file_put_contents($this->log_file, "#Fields:\tdate\ttime\ttype\tmessage\n"); 363 364 } 365 366 /** 367 * Backup Deactivation. 368 * 369 * This function is called whenever the plugin is being deactivated and removes 370 * all files and directories it created as well as the options stored in the database. 371 * It also revokes access to the Google Account associated with it and removes all scheduled events created. 372 */ 373 public function deactivate() { 374 // Revoke Google OAuth2 authorization. 375 if ( $this->goauth->is_authorized() ) 376 $this->goauth->revoke_refresh_token($this->options['refresh_token']); 377 378 // Unschedule events. 379 if ( wp_next_scheduled('backup_schedule') ) { 380 wp_clear_scheduled_hook('backup_schedule'); 381 } 382 383 // Delete options. 384 delete_option('backup_options'); 385 386 if ( ! $this->user_id ) 387 $this->user_id = get_current_user_id(); 388 389 // Remove options page user meta. 390 delete_user_meta($this->user_id, "meta-box-order_".$this->pagehook); 391 delete_user_meta($this->user_id, "closedpostboxes_".$this->pagehook); 392 delete_user_meta($this->user_id, "metaboxhidden_".$this->pagehook); 393 delete_user_meta($this->user_id, "screen_layout_".$this->pagehook); 394 395 // Delete all files created by the plugin. 396 delete_path($this->local_folder, true); 397 } 398 399 /** 400 * Filter - Add custom schedule intervals. 401 * 402 * @param array $schedules The array of defined intervals. 403 * @return array Returns the array of defined intervals after adding the custom ones. 404 */ 405 function cron_add_intervals( $schedules ) { 406 return array_merge($schedules, $this->schedules); 407 } 408 409 /** 410 * Filter - Adds a 'Settings' action link on the plugins page. 411 * 412 * @param array $links The list of links. 413 * @param string $file The plugin file to check. 414 * @return array Returns the list of links with the custom link added. 415 */ 416 function action_links( $links, $file ) { 417 if ( $file != plugin_basename(__FILE__)) 418 return $links; 419 420 $settings_link = '<a href="options-general.php?page=backup">Settings</a>'; 421 422 array_unshift($links, $settings_link); 423 424 return $links; 425 } 426 427 /** 428 * This tells WordPress we support 2 columns on the options page. 429 * 430 * @param array $columns The array of columns. 431 * @param string $screen The ID of the current screen. 432 * @return array Returns the array of columns. 433 */ 434 function on_screen_layout_columns( $columns, $screen ) { 435 if ($screen == $this->pagehook) { 436 $columns[$this->pagehook] = 2; 437 } 438 return $columns; 439 } 440 441 /** 442 * Action - Adds options page in the admin menu. 443 */ 444 function backup_menu() { 445 $this->pagehook = add_options_page('Backup Settings', 'Backup', 'manage_options', 'backup', array(&$this, 'options_page')); 446 // Hook to update options 447 add_action('load-'.$this->pagehook, array(&$this, 'options_update')); 448 // Hook to add metaboxes 449 add_action('load-'.$this->pagehook, array(&$this, 'on_load_options_page')); 450 } 451 452 /** 453 * Action - Adds meta boxes and checks if the local folder is writable. 454 */ 455 function on_load_options_page() { 456 // These scripts are needed for metaboxes to function 457 wp_enqueue_script('common'); 458 wp_enqueue_script('wp-lists'); 459 wp_enqueue_script('postbox'); 460 461 // Add the metaboxes 462 add_meta_box('metabox-authorization', 'Authorization', array(&$this, 'metabox_authorization_content'), $this->pagehook, 'side', 'core'); 463 add_meta_box('metabox-status', 'Status', array(&$this, 'metabox_status_content'), $this->pagehook, 'side', 'core'); 464 add_meta_box('metabox-advanced', 'Advanced', array(&$this, 'metabox_advanced_content'), $this->pagehook, 'normal', 'core'); 465 add_meta_box('metabox-logfile', 'Log File', array(&$this, 'metabox_logfile_content'), $this->pagehook, 'advanced', 'core'); 466 467 // Add help tabs and help sidebar 468 $screen = get_current_screen(); 469 $screen->add_help_tab(array( 470 'id' => 'overview-backup-help', // This should be unique for the screen. 471 'title' => __('Overview', $this->text_domain), 472 'content' => '<h3>' . __('Backup for WordPress', $this->text_domain) . '</h3><p>' . __('Regularly backing up a website is one of the most important duties of a webmaster and its value is only truly appreciated when things go horribly wrong (hacked website, hardware failure, software errors).', $this->text_domain) . '</p>' . 473 '<p>' . __('WordPress is a wonderful platform to build not just blogs, but also rich, powerful websites and web apps. Backing up a WordPress website was never the easiest of tasks but it has become quite effortless with the help of the Backup plugin.', $this->text_domain) . '</p>' . 474 '<h3>Backup features</h3><p>' . __('Here are some of the features of the Backup plugin:', $this->text_domain) . '</p>' . 475 '<ul><li>' . __('Backup any or all of your site\'s directories and files.', $this->text_domain) . '</li>' . 476 '<li>' . __('Ability to fine-tune the contents of the backup archive by excluding specific files and folders.', $this->text_domain) . '</li>' . 477 '<li>' . __('Create a database dump and add it to the backup.', $this->text_domain) . '</li>' . 478 '<li>' . __('It can back up locally and on Google Drive.', $this->text_domain) . '</li>' . 479 '<li>' . __('Supports automatic resuming of uploads to Google Drive.', $this->text_domain) . '</li></ul>' . 480 '<p>' . __('Rumor has it that support for uploading backups to other popular services is coming.', $this->text_domain) . '</p>' 481 ) ); 482 $screen->add_help_tab(array( 483 'id' => 'authorization-backup-help', // This should be unique for the screen. 484 'title' => __('Authorization', $this->text_domain), 485 'content' => '<p>' . sprintf(__('You can create a %1$sClient ID%2$s in the API Access section of your %3$s if you don\'t have one. A %1$sClient secret%2$s will also be generated for you as long as you select %4$sWeb Application%5$s as the application type.', $this->text_domain), '<strong>', '</strong>', '<a href="https://code.google.com/apis/console/" target="_blank">Google APIs Console</a>', '<em>', '</em>') . '</p>' . 486 '<p>' . sprintf(__('Make sure to add %s as the authorized redirect URI when asked.', $this->text_domain), '<kbd>' . $this->redirect_uri . '</kbd>') . '</p>' 487 ) ); 488 $screen->add_help_tab(array( 489 'id' => 'settings-backup-help', // This should be unique for the screen. 490 'title' => __('Backup settings', $this->text_domain), 491 'content' => '<p><strong>' . __('Local folder path', $this->text_domain) . '</strong> - ' . __('This is the path to the local filesystem directory where the plugin will store local backups, logs and other files it creates. The path has to be given absolute or relative to the WordPress root directory. Make sure the path you specify can be created by the plugin, otherwise you have to manually create it and set the right permissions for the plugin to write to it.', $this->text_domain) . '</p>' . 492 '<p><strong>' . __('Drive folder ID', $this->text_domain) . '</strong> - ' . sprintf(__('This is the resource ID of the Google Drive folder where backups will be uploaded. To get a folder\'s ID navigate to that folder in Google Drive and copy the ID from your browser\'s address bar. It is the part that comes after %s.', $this->text_domain), '<kbd>#folders/</kbd>' ) . '</p>' . 493 '<p><strong>' . __('Store a maximum of', $this->text_domain) . '</strong> - ' . __('You can choose to store as many backups as you want both locally and on Google Drive given you have enough free space. Once the maximum number of backups is reached, the oldest backups will get purged when creating new ones.', $this->text_domain) . '</p>' . 494 '<p><strong>' . __('When to back up', $this->text_domain) . '</strong> - ' . __('Selecting a backup frequency other than \'never\' will schedule backups to be performed using the WordPress cron. ', $this->text_domain) . sprintf(__('If you want to do backups using a real cron job, you should leave \'never\' selected and use the URI %s to set up the cron job.', $this->text_domain), '<kbd>' . home_url('?backup') . '</kbd>') . '</p>' 495 ) ); 496 $screen->add_help_tab(array( 497 'id' => 'advanced-backup-help', // This should be unique for the screen. 498 'title' => __('Advanced settings', $this->text_domain), 499 'content' => '<h3>' . __('Backup options', $this->text_domain) . '</h3>' . 500 '<p><strong>' . __('What to back up', $this->text_domain) . '</strong> - ' . __('By default the plugin backs up the content, uploads and plugins folders as well as the database. You can also select to back up the entire WordPress installation directory if you like.', $this->text_domain) . '</p>' . 501 '<p>' . __('On a default WordPress install the uploads and plugins folders are found inside the content folder, but they can be set up to be anywhere. Also the entire content directory can live outside the WordPress root.', $this->text_domain) . '</p>' . 502 '<p><strong>' . __('Exclude list', $this->text_domain) . '</strong> - ' . sprintf(__('This is a comma separated list of files and paths to exclude from backups. Paths can be absolute or relative to the WordPress root directory. Please note that in order to exclude a directory named %1$s that is a subdirectory of the WordPress root directory you would have to input %2$s otherwise all files and directories named %1$s will be excluded.', $this->text_domain), '<kbd>example</kbd>', '<kbd>./example</kbd>') . '</p>' . 503 '<h3>' . __('Upload options', $this->text_domain) . '</h3>' . 504 '<p><strong>' . __('Chunk size', $this->text_domain) . '</strong> - ' . __('Files are split and uploaded to Google Drive in chunks of this size. Only a size that is a multiple of 0.5 MB (512 KB) is valid. I only recommend setting this to a higher value if you have a fast upload speed but take note that the PHP will use that much more memory.', $this->text_domain) . '</p>' . 505 '<p><strong>' . __('Time limit', $this->text_domain) . '</strong> - ' . __('If possible this will be set as the time limit for uploading a file to Google Drive. Just before reaching this limit, the upload stops and an upload resume is scheduled.', $this->text_domain) . '</p>' 506 ) ); 507 508 $screen->set_help_sidebar( 509 '<p><strong>' . __('For more information', $this->text_domain) . '</strong></p>' . 510 '<p><a href="http://hel.io/wordpress/backup/">' . __('Plugin homepage', $this->text_domain) . '</a></p>' . 511 '<p><a href="http://wordpress.org/extend/plugins/backup/">' . __('Plugin page on WordPress.org', $this->text_domain) . '</a></p>' . 512 '<p></p><p>' . sprintf(__('If you find this plugin useful and want to support its development please consider %smaking a donation%s.', $this->text_domain), '<a href="http://hel.io/donate/">', '</a>') . '</p>' 513 ); 514 515 // Check if the local folder is writable 516 if ( !@is_writable($this->local_folder) ) 517 $this->messages['error'][] = sprintf(__("The local path '%s' is not writable. Please change the permissions or choose another directory.", $this->text_domain), $this->local_folder); 518 } 519 520 /** 521 * Display options page. 522 */ 523 function options_page() { 524 global $screen_layout_columns; 525 require_once('page-options.php'); 526 } 527 528 /** 529 * Render Authorization meta box. 530 */ 531 function metabox_authorization_content( $data ) { 532 if ( !$this->goauth->is_authorized() ) { ?> 533 <form action="<?php echo $this->redirect_uri; ?>" method="post"> 534 <p><?php _e('Before backups can be uploaded to Google Drive, you need to authorize the plugin and give it permission to make changes on your behalf.', $this->text_domain); ?></p> 535 <p> 536 <label for="client_id"><?php _e('Client ID', $this->text_domain); ?></label> 537 <input id="client_id" name="client_id" type='text' style="width: 99%" value='<?php echo esc_html($this->options['client_id']); ?>' /> 538 </p> 539 <label for="client_secret"><?php _e('Client secret', $this->text_domain); ?> 540 <input id="client_secret" name='client_secret' type='text' style="width: 99%" value='<?php echo esc_html($this->options['client_secret']); ?>' /> 541 </p> 542 <p> 543 <input name="authorize" type="submit" class="button-secondary" value="<?php _e('Authorize', $this->text_domain) ?>" /> 199 544 </p> 200 545 </form> 201 546 <?php } else { ?> 202 <p><?php _e( 'Authorization to use Google Drive has been granted. You can revoke it at any time by clicking the button below.', 'backup' ); ?></p> 203 <p class="submit"> 204 <a href="options-general.php?page=backup&action=auth&state=revoke" class="button-secondary"><?php _e( 'Revoke Authorization', 'backup' ); ?></a> 205 </p> 206 <?php } ?> 207 208 <form action="options-general.php?page=backup&action=update" method="post"> 209 <h3><?php _e( 'Settings', 'backup' ); ?></h3> 210 <p><?php _e( 'Please enter the name of the Google Drive folder where you want to store backups and select the frequency at which to perform backups.', 'backup' ); ?></p> 211 <p><?php _e( 'To get a folder\'s ID navigate to that folder in Google Drive and copy the ID from your browser\'s address bar. It is the part that comes after <kbd>#folders/</kbd>.', 'backup' ); ?> </p> 212 <table class="form-table"> 213 <tbody> 214 <tr valign="top"> 215 <th scope="row"><?php _e( 'Backup folder ID', 'backup' ); ?></th> 216 <td> 217 <input name='folder' type='text' class='regular-text' value='<?php echo $options['folder']; ?>' /> 218 <span class="description"><?php _e( 'Leave empty to save in root folder.', 'backup' ) ?></span> 219 </td> 220 </tr> 221 <tr valign="top"> 222 <th scope="row"><?php _e( 'Store a maximum of', 'backup' ); ?></th> 223 <td> 224 <input name='local_number' type='text' class='small-text' value='<?php echo $options['local_number']; ?>' /> <?php _e( 'backups locally and ', 'backup' ); ?> 225 <input name='drive_number' type='text' class='small-text' value='<?php echo $options['drive_number']; ?>' /> <?php _e( 'backups on Google Drive.', 'backup' ); ?> 226 </td> 227 </tr> 228 <tr valign="top"> 229 <th scope="row"><?php _e( 'When to backup', 'backup' ); ?></th> 230 <td> 231 <select name='frequency'> 232 <option value='never' <?php selected( 'never', $options['frequency'] ); ?> ><?php _e( 'Never', 'backup' ); ?></option> 233 <option value='daily' <?php selected( 'daily', $options['frequency'] ); ?> ><?php _e( 'Daily', 'backup' ); ?></option> 234 <option value='weekly' <?php selected( 'weekly', $options['frequency'] ); ?> ><?php _e( 'Weekly', 'backup' ); ?></option> 235 <option value='monthly' <?php selected( 'monthly', $options['frequency'] ); ?> ><?php _e( 'Monthly', 'backup' ); ?></option> 236 </select> 237 <span class="description"><?php _e( "Select <kbd>never</kbd> if you want to add a cron job to do backups.", 'backup' ) ?></span> 238 </td> 239 </tbody> 240 </table> 241 <p class="submit"> 242 <input name="submit" type="submit" class="button-primary" value="<?php _e('Save Changes', 'backup') ?>" /> 243 </p> 244 </form> 547 <p><?php _e('Authorization to use Google Drive has been granted. You can revoke it at any time by clicking the button below.', $this->text_domain); ?></p> 548 <p><a href="<?php echo $this->redirect_uri; ?>&state=revoke" class="button-secondary"><?php _e('Revoke authorization', $this->text_domain); ?></a></p><?php 549 } 550 } 551 552 /** 553 * Render Status meta box. 554 */ 555 function metabox_status_content( $data ) { 556 echo '<div class="misc-pub-section">' . __('Current date and time:', $this->text_domain) . '<br/><strong>' . 557 /* translators: date format, see http://php.net/date */ 558 date(__("M j, Y \a\\t H:i", $this->text_domain), $this->time) . 559 '</strong></div>' . 560 '<div class="misc-pub-section">' . __('Most recent backup:', $this->text_domain) . '<br/><strong>'; 561 if ( $this->options['last_backup'] ) 562 echo 563 /* translators: date format, see http://php.net/date */ 564 date(__("M j, Y \a\\t H:i", $this->text_domain), $this->options['last_backup']); 565 else 566 _e('never', $this->text_domain); 567 echo '</strong></div>' . 568 '<div class="misc-pub-section">' . __('Next scheduled backup:', $this->text_domain) . '<br/><strong>'; 569 if ( $next = wp_next_scheduled('backup_schedule')) 570 echo 571 /* translators: date format, see http://php.net/date */ 572 date(__("M j, Y \a\\t H:i", $this->text_domain), $next); 573 else 574 _e('never', $this->text_domain); 575 echo '</strong></div>'; 576 if ( $this->options['quota_used'] ) { 577 echo '<div class="misc-pub-section">' . __('Google Drive quota:', $this->text_domain) . '<br/><strong>'; 578 printf(__('%s of %s used', $this->text_domain), size_format($this->options['quota_used']), size_format($this->options['quota_total'] )); 579 echo '</strong></div>'; 580 } 581 echo '<div class="misc-pub-section misc-pub-section-last">' . __('Manual backup URL:', $this->text_domain) . '<br/><kbd>' . home_url('?backup') . '</kbd></div><div class="clear"></div>'; 582 } 583 584 /** 585 * Render Advanced meta box. 586 */ 587 function metabox_advanced_content( $data ) { 588 $names = array_keys($this->sources); 589 echo '<div id="the-comment-list">' . 590 '<div class="comment-item">' . 591 '<h4>' . __("Backup options", $this->text_domain) . '</h4>' . 592 '<table class="form-table">' . 593 '<tbody>' . 594 '<tr valign="top">' . 595 '<th scope="row">' . __("What to back up", $this->text_domain) . '</th>' . 596 '<td>' . 597 '<div class="feature-filter">' . 598 '<ol class="feature-group">'; 599 foreach ( $this->sources as $name => $source ) 600 echo '<li><label for="source_' . $name . '" title="' . $source['path'] . '"><input id="source_' . $name . '" name="sources[]" type="checkbox" value="' . $name . '" ' . checked($name, in_array($name, $this->options['source_list'])?$name:false, false) . ' /> ' . $source['title'] . '</label></li>'; 601 echo '</ol>' . 602 '<div class="clear">' . 603 '</div>' . 604 '</td>' . 605 '</tr>' . 606 '<tr valign="top">' . 607 '<th scope="row"><label for="exclude">' . __('Exclude list', $this->text_domain) . '</label></th>' . 608 '<td><input id="exclude" name="exclude" type="text" class="regular-text code" placeholder="' . __('Comma separated paths to exclude.', $this->text_domain) . '" value="' . esc_html(implode(', ', $this->options['exclude_list'])) . '" /></td>' . 609 '</tr>' . 610 '</tbody>' . 611 '</table>' . 612 '</div>' . 613 '<div class="comment-item">' . 614 '<h4>' . __('Upload options', $this->text_domain) . '</h4>' . 615 '<table class="form-table">' . 616 '<tbody>' . 617 '<tr valign="top">' . 618 '<th scope="row"><label for="chunk_size">' . __('Chunk size', $this->text_domain) . '</label></th>' . 619 '<td><input id="chunk_size" name="chunk_size" class="small-text" type="number" min="0.5" step="0.5" value="' . floatval($this->options['chunk_size']) . '" /> <span>' . __("MB", $this->text_domain) . '</span></td>' . 620 '</tr>' . 621 '<tr valign="top">' . 622 '<th scope="row"><label for="time_limit">' . __('Time limit', $this->text_domain) . '</label></th>' . 623 '<td><input id="time_limit" name="time_limit" class="small-text" type="number" min="5" step="5" value="' . intval($this->options['time_limit']) . '" /> <span>' . __("seconds", $this->text_domain) . '</span></td>' . 624 '</tr>' . 625 '</tbody>' . 626 '</table>' . 627 '</div>' . 628 '</div>'; 629 630 } 631 632 /** 633 * Render Log file meta box. 634 */ 635 function metabox_logfile_content( $data ) { 636 $lines = get_tail($this->log_file); 637 if ( !empty($lines) && '#' == substr($lines[0], 0, 1) ) 638 array_shift($lines); 639 if ( !empty($lines) ) { 640 $header = get_first_line($this->log_file); 641 $header = substr($header, 9); 642 $header = explode("\t", $header); 643 foreach ( $lines as $i => $l ) { 644 $lines[$i] = explode("\t", $l); 645 } 646 647 echo '<table class="widefat fixed"><thead><tr>'; 648 foreach ( $header as $i => $h ) 649 echo '<th ' . (( 3 == $i )? '' : 'class="column-rel"') . '>' . esc_html(ucfirst($h)) . '</th>'; 650 echo '</tr></thead><tbody>'; 651 foreach ( $lines as $line ) { 652 echo '<tr>'; 653 foreach ( $line as $l ) 654 echo '<td class="code">' . esc_html($l) . '</td>'; 655 echo '</tr>'; 656 } 657 echo '</tbody></table>'; 658 } 659 else 660 echo '<p>' . __("There is no log file to display or the file is empty.", $this->text_domain) . '</p>'; 661 } 662 663 /** 664 * Validates and sanitizes user submitted options and saves them. 665 */ 666 function options_update() { 667 if ( isset($_GET['action']) && 'update' == $_GET['action'] ) { 668 check_admin_referer('backup_options'); 669 670 // If we dont have a valid recurrence frequency stop function execution. 671 if ( isset($_POST['backup_frequency']) && !in_array($_POST['backup_frequency'], array_keys(wp_get_schedules()) ) && $_POST['backup_frequency'] != 'never' ) 672 wp_die(__('You were caught trying to do an illegal operation.', $this->text_domain), __('Illegal operation', $this->text_domain)); 673 674 // If we have sources that we haven't defined stop function execution. 675 if ( isset($_POST['sources']) ) { 676 $array_keys = array_keys($this->sources); 677 foreach ( $_POST['sources'] as $source ) 678 if ( !in_array($source, $array_keys) ) 679 wp_die(__('You were caught trying to do an illegal operation.', $this->text_domain), __('Illegal operation', $this->text_domain)); 680 $this->options['source_list'] = $_POST['sources']; 681 } 682 683 // Validate and save chunk size. 684 if ( isset($_POST['chunk_size']) ) { 685 $chunk_size = floatval($_POST['chunk_size']); 686 if ( 0 < $chunk_size && 0 == ($chunk_size * 10) % 5 ) 687 $this->options['chunk_size'] = $chunk_size; 688 else 689 $this->messages['error'][] = __('The chunk size must be a multiple of 0.5 MB.', $this->text_domain); 690 } 691 692 // Validate and save time limit. 693 if ( isset($_POST['time_limit']) ) { 694 $time_limit = intval($_POST['time_limit']); 695 if ( $time_limit >= 5 ) 696 $this->options['time_limit'] = $time_limit; 697 else 698 $this->messages['error'][] = __('The upload time limit must be at least 5 seconds.', $this->text_domain); 699 } 700 701 // Validate and save local and drive numbers. 702 if ( isset($_POST['local_number']) ) { 703 $local_number = intval($_POST['local_number']); 704 if ( $local_number < 0 ) 705 $this->messages['error'][] = __('The number of local backups to store must be a positive integer.', $this->text_domain); 706 if ( isset($_POST['drive_number']) ) { 707 $drive_number = intval($_POST['drive_number']); 708 if ( $drive_number < 0 ) 709 $this->messages['error'][] = __('The number of Drive backups to store must be a positive integer.', $this->text_domain); 710 elseif ( 0 == $local_number && 0 == $drive_number ) 711 $this->messages['error'][] = __('You need to store at least one local or Drive backup.', $this->text_domain); 712 else { 713 $this->options['drive_number'] = $drive_number; 714 $this->options['local_number'] = $local_number; 715 } 716 } 717 else 718 if ( 0 >= $local_number ) 719 $this->messages['error'][] = __('You need to store at least one local backup.', $this->text_domain); 720 else 721 $this->options['local_number'] = $local_number; 722 } 723 724 // Handle local folder change. 725 if ( isset($_POST['local_folder']) && $_POST['local_folder'] != $this->options['local_folder'] ) { 726 $path = absolute_path($_POST['local_folder'], ABSPATH); 727 if ( !wp_mkdir_p($path) ) 728 $this->messages['error'][] = sprintf(__('Could not create directory %s. You might want to create it manually and set the right permissions. ', $this->text_domain), '<kbd>' . $path . '</kbd>'); 729 elseif ( !@is_file($path . '/' . $this->log_filename) && false === file_put_contents($path . '/' . $this->log_filename, "#Fields:\tdate\ttime\ttype\tmessage\n") ) 730 $this->messages['error'][] = __("Could not create log file. Please check permissions.", $this->text_domain); 731 else { 732 $this->options['local_folder'] = $_POST['local_folder']; 733 $this->local_path = $path; 734 } 735 } 736 737 // Handle exlclude list. 738 if ( isset($_POST['exclude']) ) { 739 $this->options['exclude_list'] = explode(',', $_POST['exclude']); 740 foreach ( $this->options['exclude_list'] as $i => $v ) 741 $this->options['exclude_list'][$i] = trim($v); 742 } 743 744 // If we have any error messages to display don't go any further with the function execution. 745 if ( empty($this->messages['error']) ) 746 $this->messages['updated'][] = __('All changes were saved successfully.', $this->text_domain); 747 else 748 return; 749 750 if ( isset($_POST['drive_folder']) ) 751 $this->options['drive_folder'] = $_POST['drive_folder']; 752 753 754 // Handle scheduling. 755 if ( isset($_POST['backup_frequency']) && $this->options['backup_frequency'] != $_POST['backup_frequency'] ) { 756 // If we have already scheduled a backup before, clear it first. 757 if ( wp_next_scheduled('backup_schedule') ) { 758 wp_clear_scheduled_hook('backup_schedule'); 759 } 760 761 // Schedule backup if frequency is something else than never. 762 if ( $_POST['backup_frequency'] != 'never' ) { 763 wp_schedule_event($this->time, $_POST['backup_frequency'], 'backup_schedule'); 764 } 765 766 $this->options['backup_frequency'] = $_POST['backup_frequency']; 767 } 768 769 // Updating options in the database. 770 update_option('backup_options', $this->options); 771 } 772 } 773 774 /** 775 * Function to initiate backup if the appropriate page is requested. 776 * It hooks to 'template_redirect'. 777 */ 778 function manual_backup() { 779 if ( isset($_GET['backup']) ) { 780 if ( 'resume' == $_GET['backup'] ) 781 $this->backup_resume(); 782 $this->do_backup(); 783 header("HTTP/1.1 200 OK"); 784 exit; 785 } 786 } 787 788 /** 789 * Initiates the backup procedure. 790 */ 791 public function do_backup() { 792 // Check if the backup folder is writable 793 if ( !( is_dir($this->local_folder) && @is_writable($this->local_folder) ) ) { 794 $this->log('ERROR', "The directory '" . $this->local_folder . "' does not exist or it is not writable."); 795 } 796 797 // Measure the time this function takes to complete. 798 $start = microtime(true); 799 // Get the memory usage before we do anything. 800 $initial_memory = memory_get_usage(true); 801 // We might need a lot of memory for this 802 @ini_set('memory_limit', apply_filters('admin_memory_limit', WP_MAX_MEMORY_LIMIT)); 803 804 $file_name = $this->time . '.zip'; 805 $file_path = $this->local_folder . '/' . $file_name; 245 806 246 </div> 247 <?php 807 // Create database dump sql file. 808 if ( in_array('database', $this->options['source_list']) ) { 809 $this->log('NOTICE', 'Attempting to dump database to ' . $this->dump_file); 810 $dump_time = db_dump($this->dump_file); 811 812 if ( is_wp_error($dump_time) ) { 813 $this->log_wp_error($dump_time); 814 exit; 815 } 816 817 $this->log('NOTICE', "The database dump was completed successfully in " . round($dump_time, 2) . " seconds."); 818 } 819 820 $exclude = array_merge($this->options['exclude_list'], $this->exclude); 821 foreach ( $exclude as $i => $path ) 822 $exclude[$i] = absolute_path($path, ABSPATH); 823 824 $sources = array(); 825 foreach ( $this->options['source_list'] as $source ) 826 $sources[] = $this->sources[$source]['path']; 827 828 // Remove subdirectories. 829 $count = count($sources); 830 for ( $i = 0; $i < $count; $i++ ) 831 for ( $j = 0; $j < $count; $j++ ) 832 if ( $j != $i && isset($sources[$i]) && isset($sources[$j]) && is_subdir($sources[$j], $sources[$i]) ) 833 unset($sources[$j]); 834 835 // Create archive from all enabled sources. 836 $this->log('NOTICE', 'Attempting to create archive ' . $file_path); 837 $zip_time = zip($sources, $file_path, $exclude); 838 839 if ( is_wp_error($zip_time) ) { 840 $this->log_wp_error($zip_time); 841 exit; 842 } 843 844 $this->log('NOTICE', 'Archive created successfully in ' . round($zip_time, 2) . ' seconds. Archive file size is ' . size_format( filesize( $file_path ) ) ) . '.'; 845 delete_path($this->dump_file); 846 847 if ( $this->options['drive_number'] > 0 && $this->goauth->is_authorized() ) { 848 $access_token = $this->goauth->get_access_token(); 849 if ( is_wp_error($access_token) ) 850 $this->log_wp_error($access_token); 851 else { 852 853 // We need an instance of GDocs here to talk to the Google Documents List API. 854 if ( ! is_gdocs($this->gdocs) ) 855 $this->gdocs = new GDocs($access_token); 856 857 $this->gdocs->set_option('chunk_size', $this->options['chunk_size']); 858 $this->gdocs->set_option('time_limit', $this->options['time_limit']); 859 860 $this->log('NOTICE', 'Attempting to upload archive to Google Drive'); 861 $id = $this->gdocs->upload_file($file_path, $file_name, $this->options['drive_folder']); 862 if ( is_wp_error($id) ) { 863 $this->log_wp_error($id); 864 $err = $id->get_error_message('resumable'); 865 if ( ! empty($err) ) // If we are here it means we have a chance at resuming the download so schedule resume. 866 wp_schedule_single_event($this->time, 'backup_resume'); 867 } 868 else { 869 $this->log('NOTICE', 'Archive ' . $file_name . ' uploaded to Google Drive in ' . round($this->gdocs->time_taken(), 2) . ' seconds'); 870 $this->options['local_files'][] = $file_path; 871 $this->options['drive_files'][] = $id; 872 $this->options['last_backup'] = $this->time; 873 874 // Update quotas if uploading to Google Drive was successful. 875 $this->update_quota(); 876 877 // Delete excess Drive files only if we have a successful upload. 878 $this->purge_drive_files(); 879 } 880 } 881 } 882 else { 883 $this->options['local_files'][] = $file_path; 884 $this->options['last_backup'] = $this->time; 885 } 886 887 $this->purge_local_files(); 888 889 // Updating options in the database. 890 update_option('backup_options', $this->options); 891 892 // Get memory peak usage. 893 $peak_memory = memory_get_peak_usage(true); 894 $this->log('NOTICE', 'Backup process completed in ' . round(microtime(true) - $start, 2) . ' seconds. Initial PHP memory usage was ' . round($initial_memory / 1048576, 2) . ' MB and the backup process used another ' . round(($peak_memory - $initial_memory) / 1048576, 2) .' MB of RAM.'); 895 } 896 897 /** 898 * Resumes an interrupted backup upload. 899 */ 900 public function backup_resume() { 901 // Check if the backup folder is writable. 902 if ( !( is_dir($this->local_folder) && @is_writable($this->local_folder) ) ) { 903 $this->log('ERROR', "The directory '" . $this->local_folder . "' does not exist or it is not writable."); 904 } 905 $access_token = $this->goauth->get_access_token(); 906 if ( is_wp_error($access_token) ) 907 $this->log_wp_error($access_token); 908 else { 909 // We need an instance of GDocs here to talk to the Google Documents List API. 910 if ( ! is_gdocs($this->gdocs) ) 911 $this->gdocs = new GDocs($access_token); 912 $file = $this->gdocs->get_resume_item(); 913 if ( $file ) 914 $this->log('NOTICE', 'Resuming upload of ' . $file['title'] . '.'); 915 $id = $this->gdocs->resume_upload(); 916 if ( is_wp_error($id) ) { 917 $this->log_wp_error($id); 918 $err = $id->get_error_message('resumable'); 919 if ( ! empty($err) ) 920 wp_schedule_single_event($this->time, 'backup_resume'); 921 } 922 else { 923 $this->log('NOTICE', 'The archive was uploaded successfully.'); 924 $this->options['drive_files'][] = $id; 925 $this->options['last_backup'] = substr($file['title'], 0, strpos($file['title'], '.')); // take the time from the title 926 // Update quotas if uploading to Google Drive was successful. 927 $this->update_quota(); 928 $this->purge_drive_files(); 929 delete_path($file['path']); 930 // Updating options in the database. 931 update_option('backup_options', $this->options); 932 } 933 } 934 } 935 936 /** 937 * Purge Google Drive backup files. 938 */ 939 private function purge_drive_files() { 940 if ( is_gdocs($this->gdocs) ) 941 while ( count($this->options['drive_files']) > $this->options['drive_number'] ) { 942 $result = $this->gdocs->delete_resource($r = array_shift($this->options['drive_files'])); 943 if ( is_wp_error($result) ) 944 $this->log_wp_error($result); 945 else 946 $this->log('NOTICE', 'Deleted Google Drive file ' . $r); 947 } 948 return new WP_Error('missing_gdocs', "An instance of GDocs is needed to delete Google Drive resources."); 949 } 950 951 /** 952 * Purge local filesystem backup files. 953 */ 954 private function purge_local_files() { 955 while ( count($this->options['local_files']) > $this->options['local_number'] ) 956 if ( delete_path($f = array_shift($this->options['local_files'])) ) 957 $this->log('NOTICE', 'Purged backup file ' . $f); 958 else 959 $this->log('WARNING', 'Could not delete file ' . $f); 960 } 961 962 /** 963 * Updates used and total quota. 964 */ 965 private function update_quota() { 966 if ( is_gdocs($this->gdocs) ) { 967 $quota_used = $this->gdocs->get_quota_used(); 968 if ( is_wp_error($quota_used) ) 969 $this->log_wp_error($quota_used); 970 else 971 $this->options['quota_used'] = $quota_used; 972 973 $quota_total = $this->gdocs->get_quota_total(); 974 if ( is_wp_error($quota_total) ) 975 $this->log_wp_error($quota_total); 976 else 977 $this->options['quota_total'] = $quota_total; 978 } 979 else return new WP_Error('missing_gdocs', "An instance of GDocs is needed to update quota usage."); 980 } 981 982 /** 983 * Renders messages. 984 */ 985 private function get_messages_html() { 986 $ret = ''; 987 foreach ( array_keys($this->messages) as $type ) { 988 $ret .= '<div class="' . $type . '">'; 989 foreach ( $this->messages[$type] as $message ) 990 $ret .= '<p>' . $message . '</p>'; 991 $ret .= '</div>'; 992 } 993 return $ret; 994 } 995 996 /** 997 * Checks if the authorization/authentication page is requested. 998 * 999 * @return boolean Returns TRUE if the authorization page is requested, FALSE otherwise. 1000 */ 1001 function is_auth() { 1002 return ( isset( $_GET['page'] ) && 'backup' == $_GET['page'] && isset( $_GET['action'] ) && 'auth' == $_GET['action']); 1003 } 1004 1005 /** 1006 * Handles Google OAuth2 requests 1007 */ 1008 function auth() { 1009 if ( isset($_GET['state']) ) { 1010 if ( 'token' == $_GET['state'] ) { 1011 $refresh_token = $this->goauth->request_refresh_token(); 1012 if ( is_wp_error($refresh_token) ) { 1013 $this->messages['error'][] = __("Authorization failed!", $this->text_domain); 1014 $this->messages['error'][] = $refresh_token->get_error_message(); 1015 } 1016 else { 1017 $this->options['refresh_token'] = $refresh_token; 1018 $this->messages['updated'][] = __("Authorization was successful.", $this->text_domain); 1019 1020 // Authorization was successful, so create an instance of GDocs and update quota. 1021 $this->gdocs = new GDocs($this->goauth->get_access_token()); 1022 $result = $this->update_quota(); 1023 update_option('backup_options', $this->options); 1024 } 1025 } 1026 elseif ( 'revoke' == $_GET['state'] ) { 1027 $result = $this->goauth->revoke_refresh_token(); 1028 if ( is_wp_error($result) ) { 1029 $this->messages['error'][] = __("Could not revoke authorization!", $this->text_domain); 1030 $this->messages['error'][] = $result->get_error_message(); 1031 } 1032 else { 1033 $this->options['refresh_token'] = ''; 1034 update_option('backup_options', $this->options); 1035 $this->messages['updated'][] = __("Authorization has been revoked.", $this->text_domain); 1036 } 1037 } 1038 } 1039 else { 1040 if ( !isset($_POST['client_id']) || !isset($_POST['client_secret']) ) 1041 $this->messages['error'][] = __("You need to specify a 'Client ID' and a 'Client secret' in order to authorize the Backup plugin.", $this->text_domain); 1042 else { 1043 $this->options['client_id'] = $_POST['client_id']; 1044 $this->options['client_secret'] = $_POST['client_secret']; 1045 update_option('backup_options', $this->options); 1046 $this->goauth = new GOAuth($this->options['client_id'], $this->options['client_secret'], $this->redirect_uri); 1047 $res = $this->goauth->request_authorization($this->goauth_scope, 'token'); 1048 if ( is_wp_error($res) ) 1049 $this->log_wp_error($res); 1050 exit; 1051 } 1052 } 1053 } 1054 1055 /** 1056 * Sends the first error message from a WP_Error to be written to log. 1057 * @param WP_Error $wp_error Instance of WP_Error. 1058 * @return boolean Returns TRUE on success, FALSE on failure. 1059 */ 1060 function log_wp_error( $wp_error ) { 1061 if ( is_wp_error($wp_error) ) { 1062 return $this->log('ERROR', $wp_error->get_error_message() . ' (' . $wp_error->get_error_code() . ')'); 1063 } 1064 return false; 1065 } 1066 1067 /** 1068 * Custom logging function for the backup plugin. 1069 * 1070 * @param string $type Type of message that we are logging. Should be 'NOTICE', 'WARNING' or 'ERROR'. 1071 * @param string $message The message we are logging. 1072 * @param string $file The file where the function was called from. The funciton should always be called with __FILE__ as the $file parameter. 1073 * @param string $line The line where the function was called from. The function should always be called with __LINE__ as the $line parameter. 1074 * @return boolean Returns TRUE on success, FALSE on failure. 1075 */ 1076 function log( $type, $message, $file = '', $line = '' ) { 1077 return error_log(date("Y-m-d\tH:i:s") . "\t" . $type . "\t" . $message . "\n", 3, $this->log_file); 1078 } 248 1079 } 249 1080 250 add_action('init', 'backup_update'); 251 function backup_update() { 252 if ( is_admin() && isset( $_GET['page'] ) && $_GET['page'] == 'backup' && isset( $_GET['action'] ) && $_GET['action'] == 'update' ) { 253 254 $options = get_option('backup_options' ); 255 256 // If we dont have a valid recurrence frequency stop function execution 257 if ( !in_array($_POST['frequency'], array_keys( wp_get_schedules() ) ) && $_POST['frequency'] != 'never' ) 258 return false; 259 260 // If number isn't an integer stop function execution 261 $local_number = intval( $_POST['local_number'] ); 262 $drive_number = intval( $_POST['drive_number'] ); 263 if ( $local_number < 0 || $drive_number < 0 ) 264 return false; 265 266 $options['folder'] = $_POST['folder']; 267 $options['local_number'] = $local_number; 268 $options['drive_number'] = $drive_number; 269 270 if ( $options['frequency'] != $_POST['frequency'] ) { 271 // If we have already scheduled a backup before, clear it first 272 if ( wp_next_scheduled( 'backup_schedule' ) ) { 273 wp_clear_scheduled_hook('backup_schedule'); 274 } 275 276 // Schedule backup if frequency is something else than never 277 if ( $_POST['frequency'] != 'never' ) { 278 wp_schedule_event( current_time( 'timestamp' ), $_POST['frequency'], 'backup_schedule'); 279 } 280 } 281 282 $options['frequency'] = $_POST['frequency']; 283 update_option( 'backup_options', $options ); 284 } 285 } 286 287 /** 288 * Function to initiate backup if the appropriate page is requested. 289 * It hooks to 'template_redirect'. 290 */ 291 function manual_backup() { 292 if ( $_SERVER['REQUEST_URI'] == '/backup/' || $_SERVER['REQUEST_URI'] == '/backup' ) { 293 do_backup(); 294 header("HTTP/1.1 200 OK"); 295 exit; 296 } 297 } 298 add_action('template_redirect', 'manual_backup'); 299 300 add_action( 'backup_schedule', 'do_backup' ); 301 function do_backup() { 302 $options = get_option( 'backup_options' ); 303 $backup_time = time(); 304 $file_name = $backup_time . '.zip'; 305 $file_path = WP_CONTENT_DIR . '/backup/' . $file_name; 306 307 // Create database dump sql file 308 db_dump(); 309 310 // Create archive from wp-content 311 backup_log( 'NOTICE', 'Attempting to create archive ' . $file_path ); 312 $timer_start = microtime( true ); 313 $result = zip( WP_CONTENT_DIR . '/', $file_path, WP_CONTENT_DIR . '/backup' ); 314 315 if ( !$result ) { 316 backup_log( 'ERROR', "There was a problem creating the zip file!", __FILE__, __LINE__ ); 317 } 318 else { 319 backup_log( 'NOTICE', 'Archive created successfully in ' . ( microtime( true ) - $timer_start ) . ' seconds.' ); 320 $options['last_backup'] = $backup_time; 321 $options['local_files'][] = $file_path; 322 if ( $options['drive_number'] > 0 && $options['token'] && $options['client_id'] && $options['client_secret'] ) { 323 if ( $access = access_token( $options['token'], $options['client_id'], $options['client_secret'] ) ) { 324 backup_log( 'NOTICE', 'Attempting to upload archive to Google Drive' ); 325 $timer_start = microtime( true ); 326 if ( ! $id = upload_file( $file_path, $file_name, $options['folder'], $access ) ) 327 backup_log( 'ERROR', 'Failed to upload archive to Google Drive!' ); 328 else { 329 backup_log( 'NOTICE', 'Archive ' . $file_name . ' uploaded to Google Drive in ' . ( microtime( true ) - $timer_start ) . ' seconds' ); 330 $options['drive_files'][] = $id; 331 } 332 } 333 else { 334 backup_log( 'ERROR', 'Did not receive an access token from Google!', __FILE__, __LINE__ ); 335 } 336 } 337 338 // Delete excess Drive files 339 while ( count( $options['drive_files'] ) > $options['drive_number'] ) 340 if ( delete_file( $r = array_shift( $options['drive_files'] ), $access ) ) 341 backup_log( 'NOTICE', 'Deleted Google Drive file ' . $r ); 342 else 343 backup_log( 'WARNING', 'Could not delete Google Drive file ' . $r ); 344 345 // Delete excess local files 346 while ( count( $options['local_files'] ) > $options['local_number'] ) 347 if ( unlink( $f = array_shift( $options['local_files'] ) ) ) 348 backup_log( 'NOTICE', 'Deleted old backup file ' . $f ); 349 else 350 backup_log( 'WARNING', 'Could not delete file ' . $f ); 351 352 update_option( 'backup_options', $options ); 353 } 354 } 355 356 /** 357 * Function to upload a file to Google Drive 358 * 359 * @param string $file Path to the file that is to be uploaded 360 * @param string $title Title to be given to the file 361 * @param string $parent ID of the folder in which to upload the file 362 * @param string $token Access token from Google Account 363 * @return boolean Returns TRUE on success, FALSE on failure 364 */ 365 function upload_file( $file, $title, $parent = '', $token) { 366 367 $size = filesize( $file ); 368 369 $content = '<?xml version=\'1.0\' encoding=\'UTF-8\'?> 370 <entry xmlns="http://www.w3.org/2005/Atom" xmlns:docs="http://schemas.google.com/docs/2007"> 371 <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/docs/2007#file"/> 372 <title>' . $title . '</title> 373 </entry>'; 374 375 $header = array( 376 'Authorization: Bearer ' . $token, 377 'Content-Length: ' . strlen( $content ), 378 'Content-Type: application/atom+xml', 379 'X-Upload-Content-Type: application/zip', 380 'X-Upload-Content-Length: ' . $size, 381 'GData-Version: 3.0' 382 ); 383 384 $context = array( 385 'http' => array( 386 'ignore_errors' => true, 387 'follow_location' => false, 388 'method' => 'POST', 389 'header' => join( "\r\n", $header ), 390 'content' => $content 391 ) 392 ); 393 394 $url = get_resumable_create_media_link( $token, $parent ); 395 if ( $url ) 396 $url .= '?convert=false'; // needed to upload a file 397 else { 398 backup_log( 'ERROR', 'Could not retrieve resumable create media link.', __FILE__, __LINE__ ); 399 return false; 400 } 401 402 $result = @file_get_contents( $url, false, stream_context_create( $context ) ); 403 if ( $result !== FALSE ) { 404 if ( strpos( $response = array_shift( $http_response_header ), '200' ) ) { 405 $response_header = array(); 406 foreach ( $http_response_header as $header_line ) { 407 list( $key, $value ) = explode( ':', $header_line, 2 ); 408 $response_header[trim( $key )] = trim( $value ); 409 } 410 if ( isset( $response_header['Location'] ) ) { 411 $next_location = $response_header['Location']; 412 $pointer = 0; 413 $max_chunk_size = 524288; 414 while ( $pointer < $size - 1 ) { 415 $chunk = file_get_contents( $file, false, NULL, $pointer, $max_chunk_size ); 416 $next_location = upload_chunk( $next_location, $chunk, $pointer, $size, $token ); 417 if( $next_location === false ) { 418 return false; 419 } 420 // if object it means we have our simpleXMLElement response 421 if ( is_object( $next_location ) ) { 422 // return resource Id 423 return substr( $next_location->children( "http://schemas.google.com/g/2005" )->resourceId, 5 ); 424 } 425 $pointer += strlen( $chunk ); 426 427 } 428 } 429 } 430 else { 431 backup_log( 'ERROR', 'Bad response: ' . $response . ' Response header: ' . var_export( $response_header, true ) . ' Response body: ' . $result . ' Request URL: ' . $url, __FILE__, __LINE__ ); 432 return false; 433 } 434 } 435 else { 436 backup_log( 'ERROR', 'Unable to request file from ' . $url, __FILE__, __LINE__ ); 437 } 438 } 439 /** 440 * Handles the upload to Google Drive of a single chunk of a file 441 * 442 * @param string $location URL where the chunk needs to be uploaded 443 * @param string $chunk Part of the file to upload 444 * @param integer $pointer The byte number marking the beginning of the chunk in file 445 * @param integer $size The size of the file the chunk is part of, in bytes 446 * @param string $token Google Account access token 447 * @return string|boolean The funcion returns the location where the next chunk needs to be uploaded, TRUE if the last chunk was uploaded or FALSE on failure 448 */ 449 function upload_chunk( $location, $chunk, $pointer, $size, $token ) { 450 $chunk_size = strlen( $chunk ); 451 $bytes = (string)$pointer . '-' . (string)($pointer + $chunk_size - 1) . '/' . (string)$size; 452 $header = array( 453 'Authorization: Bearer ' . $token, 454 'Content-Length: ' . $chunk_size, 455 'Content-Type: application/zip', 456 'Content-Range: bytes ' . $bytes, 457 'GData-Version: 3.0' 458 ); 459 $context = array( 460 'http' => array( 461 'ignore_errors' => true, 462 'follow_location' => false, 463 'method' => 'PUT', 464 'header' => join( "\r\n", $header ), 465 'content' => $chunk 466 ) 467 ); 468 469 $result = @file_get_contents( $location, false, stream_context_create( $context ) ); 470 471 if ( isset( $http_response_header ) ) { 472 $response = array_shift( $http_response_header ); 473 $headers = array(); 474 foreach ( $http_response_header as $header_line ) { 475 list( $key, $value ) = explode( ':', $header_line, 2 ); 476 $headers[trim( $key )] = trim( $value ); 477 } 478 479 if ( strpos( $response, '308' ) ) { 480 if ( isset( $headers['Location'] ) ) { 481 return $headers['Location']; 482 } 483 else 484 return $location; 485 } 486 elseif ( strpos( $response, '201' ) ) { 487 $xml = simplexml_load_string( $result ); 488 if ( $xml === false ) { 489 backup_log( 'ERROR', 'Could not create SimpleXMLElement from ' . $result, __FILE__, __LINE__ ); 490 return false; 491 } 492 else 493 return $xml; 494 } 495 else { 496 backup_log( 'ERROR', 'Bad response: ' . $response, __FILE__, __LINE__ ); 497 return false; 498 } 499 } 500 else { 501 backup_log( 'ERROR', 'Received no response from ' . $location . ' while trying to upload bytes ' . $bytes ); 502 return false; 503 } 504 } 505 506 /** 507 * Deletes a file from Google Drive 508 * 509 * @param string $id Gdata resource Id of the file to be deleted 510 * @param string $token Google Account access token 511 * @return boolean Returns TRUE on success, FALSE on failure 512 */ 513 function delete_file( $id, $token ) { 514 $header = array( 515 'If-Match: *', 516 'Authorization: Bearer ' . $token, 517 'GData-Version: 3.0' 518 ); 519 $context = array( 520 'http' => array( 521 'method' => 'DELETE', 522 'header' => join( "\r\n", $header ) 523 ) 524 ); 525 stream_context_set_default( $context ); 526 $headers = get_headers( 'https://docs.google.com/feeds/default/private/full/' . $id . '?delete=true',1 ); 527 528 if ( strpos( $headers[0], '200' ) ) 529 return true; 530 return false; 531 } 532 /** 533 * Get the resumable-create-media link needed to upload files 534 * 535 * @param string $token The Google Account access token 536 * @param string $parent The Id of the folder where the upload is to be made. Default is empty string. 537 * @return string|boolean Returns a link on success, FALSE on failure. 538 */ 539 function get_resumable_create_media_link( $token, $parent = '' ) { 540 $header = array( 541 'Authorization: Bearer ' . $token, 542 'GData-Version: 3.0' 543 ); 544 $context = array( 545 'http' => array( 546 'ignore_errors' => true, 547 'method' => 'GET', 548 'header' => join( "\r\n", $header ) 549 ) 550 ); 551 $url = 'https://docs.google.com/feeds/default/private/full'; 552 553 if ( $parent ) 554 $url .= '/' . $parent; 555 556 $result = @file_get_contents( $url, false, stream_context_create( $context ) ); 557 558 if ( $result !== false ) { 559 $xml = simplexml_load_string( $result ); 560 if ( $xml === false ) { 561 backup_log( 'ERROR', 'Could not create SimpleXMLElement from ' . $result, __FILE__, __LINE__ ); 562 return false; 563 } 564 else 565 foreach ( $xml->link as $link ) 566 if ( $link['rel'] == 'http://schemas.google.com/g/2005#resumable-create-media' ) 567 return $link['href']; 568 } 569 return false; 570 } 571 572 /** 573 * Handle Google OAuth 2.0 574 */ 575 add_action('init', 'backup_auth'); 576 function backup_auth() { 577 if ( is_admin() && isset( $_GET['page'] ) && $_GET['page'] == 'backup' && isset( $_GET['action'] ) && $_GET['action'] == 'auth' ) { 578 if ( isset( $_GET['state'] ) ) { 579 if ( $_GET['state'] == 'token' ) 580 auth_token(); 581 else if ( $_GET['state'] == 'revoke' ) 582 auth_revoke(); 583 } 584 else { 585 $options = get_option( 'backup_options' ); 586 $options['client_id'] = $_POST['client_id']; 587 $options['client_secret'] = $_POST['client_secret']; 588 update_option( 'backup_options', $options ); 589 auth_request(); 590 } 591 } 592 } 593 594 /** 595 * Acquire single-use authorization code from Google OAuth 2.0 596 */ 597 function auth_request() { 598 $options = get_option( 'backup_options' ); 599 $params = array( 600 'response_type' => 'code', 601 'client_id' => $options['client_id'], 602 'redirect_uri' => admin_url('options-general.php?page=backup&action=auth'), 603 'scope' => 'https://www.googleapis.com/auth/drive.file https://docs.google.com/feeds/ https://docs.googleusercontent.com/ https://spreadsheets.google.com/feeds/', 604 'state' => 'token', 605 'access_type' => 'offline', 606 ); 607 header('Location: https://accounts.google.com/o/oauth2/auth?'.http_build_query($params)); 608 } 609 610 /** 611 * Get a Google account refresh token using the code received from auth_request 612 */ 613 function auth_token() { 614 $options = get_option( 'backup_options' ); 615 if( isset( $_GET['code'] ) ) { 616 $context = array( 617 'http' => array( 618 'method' => 'POST', 619 'header' => 'Content-type: application/x-www-form-urlencoded', 620 'content' => http_build_query( array( 621 'code' => $_GET['code'], 622 'client_id' => $options['client_id'], 623 'client_secret' => $options['client_secret'], 624 'redirect_uri' => admin_url('options-general.php?page=backup&action=auth'), 625 'grant_type' => 'authorization_code' 626 ) ) 627 ) 628 ); 629 $result = @file_get_contents('https://accounts.google.com/o/oauth2/token', false, stream_context_create($context)); 630 if($result) { 631 $result = json_decode( $result, true ); 632 if ( isset( $result['refresh_token'] ) ) { 633 $options['token'] = $result['refresh_token']; // Save token 634 update_option('backup_options', $options); 635 header('Location: '.admin_url('options-general.php?page=backup&message=' . __( 'Authorization was successful.', 'backup' ) ) ); 636 } 637 else 638 header('Location: '.admin_url('options-general.php?page=backup&error=' . __( 'No refresh token was received!', 'backup' ) ) ); 639 } 640 else 641 header('Location: '.admin_url('options-general.php?page=backup&error=' . __( 'Bad response!', 'backup' ) ) ); 642 } 643 else 644 header('Location: '.admin_url('options-general.php?page=backup&error=' . __( 'Authrization failed!', 'backup' ) ) ); 645 } 646 647 /** 648 * Get a Google account access token using the refresh token 649 */ 650 function access_token( $token, $client_id, $client_secret ) { 651 $context = array( 652 'http' => array( 653 'method' => 'POST', 654 'header' => 'Content-type: application/x-www-form-urlencoded', 655 'content' => http_build_query( array( 656 'refresh_token' => $token, 657 'client_id' => $client_id, 658 'client_secret' => $client_secret, 659 'grant_type' => 'refresh_token' 660 ) ) 661 ) 662 ); 663 $result = @file_get_contents('https://accounts.google.com/o/oauth2/token', false, stream_context_create($context)); 664 if($result) { 665 $result = json_decode( $result, true ); 666 if ( isset( $result['access_token'] ) ) 667 return $result['access_token']; 668 else 669 return false; 670 } 671 else 672 return false; 673 } 674 675 /** 676 * Revoke a Google account refresh token 677 */ 678 function auth_revoke() { 679 $options = get_option( 'backup_options' ); 680 @file_get_contents( 'https://accounts.google.com/o/oauth2/revoke?token=' . $options['token'] ); 681 $options['token'] = ''; 682 update_option( 'backup_options', $options ); 683 header( 'Location: '.admin_url( 'options-general.php?page=backup&message=' . __( 'Authorization revoked.', 'backup' ) ) ); 684 } 685 686 /** 687 * Create a Zip archive from a file or directory 688 * @param string $source File or Directory to be archived 689 * @param string $destination Destination file where the archive will be stored 690 * @param array $exclude Folder to exclude from archive, defaults to empty string 691 */ 692 function zip( $source, $destination, $exclude = '' ) { 693 if ( !file_exists( $source ) ) { 694 backup_log( 'ERROR', 'Source file or directory to be archived does not exist.', __FILE__, __LINE__ ); 695 return false; 696 } 697 698 $zip = new ZipArchive(); 699 if ( $res = $zip->open( $destination, ZIPARCHIVE::CREATE ) != true ) { 700 backup_log('ERROR', $res, __FILE__, __LINE__); 701 return false; 702 } 703 704 $source = str_replace( '\\', '/', realpath( $source ) ); 705 706 if ( is_dir( $source ) === true ) { 707 $files = directory_list( $source, $exclude, true ); 708 709 foreach ( $files as $file ) { 710 $file = str_replace( '\\', '/', realpath( $file ) ); 711 if ( is_dir( $file ) === true ) { 712 $zip->addEmptyDir( str_replace( $source . '/', '', $file . '/' ) ); 713 } 714 else if ( is_file( $file ) === true ) { 715 $zip->addFile( $file, str_replace( $source . '/', '', $file ) ); 716 } 717 } 718 } 719 else if (is_file($source) === true) { 720 $zip->addFromString(basename($source), file_get_contents($source)); 721 } 722 723 return $zip->close(); 724 } 725 726 /** 727 * directory_list 728 * 729 * return an array containing all files/directories at a file system path 730 * 731 * @param string $base_path Absolute or relative path to the base directory 732 * @param string $exclude Pipe delimited string of files to always ignore 733 * @param boolean $recursive Descend directory to the bottom? 734 * @return array $result_list Array or false 735 */ 736 function directory_list($base_path, $exclude = "", $recursive = true) { 737 $base_path = rtrim($base_path, "/") . "/"; 738 739 if ( !is_dir( $base_path ) ) { 740 backup_log( 'ERROR', __FUNCTION__ . $base_path . " is not a directory.", __FILE__, __LINE__ ); 741 return false; 742 } 743 744 $exclude_files = array( ".", "..", ".DS_Store", ".svn" ); 745 $exclude_paths = explode( "|", $exclude ); 746 747 $result_list = array(); 748 749 if (!$folder_handle = opendir($base_path)) { 750 backup_log( 'ERROR', __FUNCTION__ . "Could not open directory at: " . $base_path . ".", __FILE__, __LINE__ ); 751 return false; 752 } 753 else { 754 while ( false !== ( $filename = readdir( $folder_handle ) ) ) { 755 if ( !in_array( $filename, $exclude_files ) && !in_array( $base_path . $filename, $exclude_paths ) ) { 756 if ( is_dir( $base_path . $filename . "/" ) ) { 757 $result_list[] = $base_path . $filename; 758 if( $recursive ) { 759 $temp_list = directory_list( $base_path . $filename . "/", $exclude, $recursive); 760 if ( ! empty( $temp_list ) ) 761 foreach ( $temp_list as $item ) 762 $result_list[] = $item; 763 } 764 } else { 765 $result_list[] = $base_path . $filename; 766 } 767 } 768 } 769 closedir($folder_handle); 770 771 return $result_list; 772 } 773 } 774 775 /** 776 * Dump the WordPress database to a sql file 777 * 778 * @return boolean Returns TRUE on success, FALSE on failure 779 */ 780 function db_dump() { 781 global $wpdb; 782 $dump_file = WP_CONTENT_DIR . '/backup.sql'; 783 784 backup_log( 'NOTICE', 'Attempting to dump database to ' . $dump_file ); 785 $timer_start = microtime( true ); 786 787 $handle = fopen( $dump_file, 'wb' ); 788 789 if ( ! $handle ) 790 backup_log( 'ERROR', 'Could not open ' . $dump_file . ' for writing.', __FILE__, __LINE__ ); 791 else { 792 793 fwrite( $handle, "/**\n" ); 794 fwrite( $handle, " * SQL Dump created with Backup for WordPress\n" ); 795 fwrite( $handle, " *\n" ); 796 fwrite( $handle, " * http://hel.io/wordpress/backup\n" ); 797 fwrite( $handle, " */\n\n" ); 798 799 fwrite( $handle, "/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n" ); 800 fwrite( $handle, "/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n" ); 801 fwrite( $handle, "/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n" ); 802 fwrite( $handle, "/*!40101 SET NAMES " . DB_CHARSET . " */;\n" ); 803 fwrite( $handle, "/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;\n" ); 804 fwrite( $handle, "/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;\n" ); 805 fwrite( $handle, "/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;\n\n" ); 806 807 808 $tables = $wpdb->get_results( "SHOW TABLES", ARRAY_A ); 809 810 if ( empty( $tables ) ) 811 backup_log( 'ERROR', 'The query "SHOW TABLES" did not return any tables.', __FILE__, __LINE__ ); 812 else { 813 foreach ( $tables as $table_array ) { 814 $table = array_shift( array_values( $table_array ) ); 815 $create = $wpdb->get_var( "SHOW CREATE TABLE " . $table, 1 ); 816 817 backup_log( 'NOTICE', 'Dumping table `' . $table . '`' ); 818 819 fwrite( $handle, "/* Dump of table `" . $table . "`\n" ); 820 fwrite( $handle, " * ------------------------------------------------------------*/\n\n" ); 821 822 fwrite( $handle, "DROP TABLE IF EXISTS `" . $table . "`;\n\n" . $create . ";\n\n" ); 823 824 $data = $wpdb->get_results("SELECT * FROM `" . $table . "`", ARRAY_A ); 825 826 if ( ! empty( $data ) ) { 827 fwrite( $handle, "LOCK TABLES `" . $table . "` WRITE;\n" ); 828 if ( strpos( $create, 'MyISAM' ) !== false ) 829 fwrite( $handle, "/*!40000 ALTER TABLE `".$table."` DISABLE KEYS */;\n\n" ); 830 foreach ( $data as $entry ) { 831 foreach ( $entry as $key => $value ) { 832 if ( $value === NULL ) 833 $entry[$key] = "NULL"; 834 elseif ( $value === "" || $value === false ) 835 $entry[$key] = "''"; 836 elseif ( !is_numeric( $value ) ) 837 $entry[$key] = "'" . mysql_real_escape_string($value) . "'"; 838 } 839 fwrite( $handle, "INSERT INTO `" . $table . "` ( " . implode( ", ", array_keys( $entry ) ) . " ) VALUES ( " . implode( ", ", $entry ) . " );\n" ); 840 } 841 if ( strpos( $create, 'MyISAM' ) !== false ) 842 fwrite( $handle, "\n/*!40000 ALTER TABLE `".$table."` ENABLE KEYS */;" ); 843 fwrite( $handle, "\nUNLOCK TABLES;\n\n" ); 844 } 845 } 846 } 847 848 fwrite( $handle, "/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;\n" ); 849 fwrite( $handle, "/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;\n" ); 850 fwrite( $handle, "/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;\n" ); 851 fwrite( $handle, "/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;\n" ); 852 fwrite( $handle, "/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;\n" ); 853 fwrite( $handle, "/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;\n" ); 854 855 fclose( $handle ); 856 backup_log( 'NOTICE', 'Database dump completed in ' . ( microtime( true ) - $timer_start ) . ' seconds.' ); 857 return true; 858 } 859 return false; 860 } 861 862 /** 863 * Recursively remove a directory 864 * 865 * @param string $dir The path to the directory to be removed 866 */ 867 function rrmdir( $dir ) { 868 foreach ( glob( $dir . '/*' ) as $file ) { 869 if ( is_dir( $file ) ) 870 rrmdir( $file ); 871 else 872 unlink( $file ); 873 } 874 rmdir( $dir ); 875 } 876 877 /** 878 * Custom logging function for the backup plugin 879 * 880 * @param string $type Type of message that we are logging. Should be 'NOTICE', 'WARNING' or 'ERROR'. 881 * @param string $message The message we are logging 882 * @param string $file The file where the function was called from. The funciton should always be called with __FILE__ as the $file parameter. 883 * @param string $line The line where the function was called from. The function should always be called with __LINE__ as the $line parameter. 884 * @return boolean Returns TRUE on success, FALSE on failure. 885 */ 886 function backup_log( $type, $message, $file = '', $line = '' ) { 887 return error_log( date( "Y-m-d\tH:i:s" ) . "\t" . $type . "\t" . $message . "\t" . $file . "\t" . $line . "\n", 3, WP_CONTENT_DIR . '/backup/backup.log' ); 888 } 1081 $backup = new Backup(); 1082 1083 } //end if -
backup/trunk/readme.txt
r549264 r558116 3 3 Donate link: http://hel.io/donate/ 4 4 Tags: backup, back up, Google Drive, Drive backup, WordPress backup 5 Requires at least: 3.0 6 Tested up to: 3.3.2 7 Stable tag: 1.1.5 5 Requires at least: 3.4 6 Tested up to: 3.4 7 Stable tag: 2.0 8 License: GPLv3 9 License URI: http://www.gnu.org/licenses/gpl.html 8 10 9 11 Make backups of your Wordpress site to Google Drive. … … 11 13 == Description == 12 14 13 Backup is a plugin that provides backup capabilities for Wordpress. Backups are `zip` archives of the `wp-content` directory created locally and uploaded to a folder of your choosing on Google Drive. 15 Version 2.0 is out and it's full of improvements and new features! 14 16 15 Before creating the archive Backup dumps the database to a `sql` file inside `wp-content` so that it gets aded to the `zip` file. 17 If you use this plugin and find it useful please consider [donating](http://hel.io/donate/ "Make a donation for your favorite WordPress plugin."). I have invested (and continue to do so) a lot of time and effort into making this a useful and polished product even though at the moment I have no source of income. Even a small contribution helps a lot. 18 19 Backup is a plugin that provides backup capabilities for Wordpress. Backups are `zip` archives created locally and uploaded to a folder of your choosing on Google Drive. 20 21 You are in total control of what files and directories get backed up. 16 22 17 23 == Installation == 18 24 25 The plugin requires WordPress 3.4 and is installed like any other plugin. 26 19 27 1. Upload the plugin to the `/wp-contents/plugins/` folder. 20 28 2. Activate the plugin from the 'Plugins' menu in WordPress. 21 3. Configure the plugin by following the instructions from the `Backup` settings page. 29 3. Configure the plugin by following the instructions on the `Backup` settings page. 30 31 If you need support configuring the plugin click on the `help` button on the top right of the settings page. 32 33 == Frequently Asked Questions == 34 35 = Do you plan to support backing up to other services? = 36 37 I am thinking of adding more services and will probably do so depending on user demand. 38 39 = Does the plugin work with versions of WordPress below 3.4? = 40 41 Apart from not being able to purge backups from Google Drive, Backup should work well with WordPress versions 3.0 and up. I do recommend upgrading to WordPress 3.4 though. 22 42 23 43 == Screenshots == … … 26 46 27 47 == Changelog == 48 49 = 2.0 = 50 * Rewrote 95% of the plugin to make it more compatible with older PHP versions, more portable and cleaner. It now uses classes and functions already found in WordPress where possible. 51 * Interrupted backup uploads to Google Drive will resume automatically on the next WordPress load. 52 * Revamped the settings page. You can now choose between one and two column layout. Added meta boxes that can be hidden, shown or closed individually as well as moved between columns. 53 * Added contextual help on the settings page. 54 * Added ability to select which WordPress directories to backup. 55 * Added ability to exclude specific files or directories from being backed up. 56 * Added option not to backup the database. 57 * Displaying used quota and total quota on the settings page. 58 * Changed the manual backup URI so that it now works for WordPress installations where pretty permalinks are disabled. 59 * Optimized memory usage. 60 * Added PclZip as a fallback for creating archives. 61 * Can now configure chunk sizes for Google Drive uploads. 62 * Added option to set time limit when uploading to Google Drive. 63 * You can now view the log file directly inside the settings page. 64 28 65 = 1.1.5 = 29 66 * You can now chose not to upload backups to Google Drive by entering `0` in the appropriate field on the settings page. … … 31 68 32 69 = 1.1.3 = 33 * Fixed some issues created by the `1.1.2` update 70 * Fixed some issues created by the `1.1.2` update. 34 71 35 72 = 1.1.2 = 36 * Added the ability to store a different number of backups locally then on Google Drive 37 * On deactivation the plugin deletes all traces of itself (backups stored locally, options) and revokes access to the Google Account 38 * Fixed some more frequency issues 73 * Added the ability to store a different number of backups locally then on Google Drive. 74 * On deactivation the plugin deletes all traces of itself (backups stored locally, options) and revokes access to the Google Account. 75 * Fixed some more frequency issues. 39 76 40 77 = 1.1.1 = 41 * Fixed mo thly backup frequency78 * Fixed monthly backup frequency. 42 79 43 80 = 1.1 = 44 * Added ab bility to backup database. Database dumps are saved to a `sql` file in the `wp-content` folder and added to the backup archive.81 * Added ability to backup database. Database dumps are saved to a `sql` file in the `wp-content` folder and added to the backup archive. 45 82 * Added a page ( `/backup/` ) which can be used to trigger manual backups or used in cron jobs. 46 * Added ab bility to store a maximum of `n` backups.83 * Added ability to store a maximum of `n` backups. 47 84 * Displaying dates and times of last performed backups and next scheduled backups on the settings page as well as a link to download the most recent backup and the URL for doing manual backups (and cron jobs). 48 * Created a separate log file to log every action and error specific to the plugin 49 * Cleaned the code up a bit and added DocBlock 85 * Created a separate log file to log every action and error specific to the plugin. 86 * Cleaned the code up a bit and added DocBlock. 50 87 51 88 = 1.0 =
Note: See TracChangeset
for help on using the changeset viewer.