Plugin Directory

Changeset 558116


Ignore:
Timestamp:
06/14/2012 07:38:44 PM (13 years ago)
Author:
hel.io
Message:

Rewrote the plugin, added a lot of features.

Location:
backup/trunk
Files:
5 added
3 edited

Legend:

Unmodified
Added
Removed
  • backup/trunk/backup.php

    r549327 r558116  
    22/*
    33Plugin Name: Backup
    4 Version: 1.1.6
     4Version: 2.0
    55Plugin URI: http://hel.io/wordpress/backup/
    66Description: Backup your WordPress website to Google Drive.
    77Author: Sorin Iclanzan
    88Author URI: http://hel.io/
     9License: GPL3
    910*/
    1011
    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.
     31if ( is_admin() || defined('DOING_CRON') || isset($_GET['doing_wp_cron']) || isset($_GET['backup']) || isset($_GET['resume_backup']) ) {
     32
     33// Load required classes.
     34if ( ! class_exists('GOAuth') )
     35    require_once('class-goauth.php');
     36if ( ! class_exists('GDocs') )
     37    require_once('class-gdocs.php');
     38
     39// Load helper functions
     40require_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 */
     52class 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)
    38224            )
    39225        );
    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) ?>" />
    199544            </p>
    200545        </form>
    201546        <?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;
    245806       
    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    }
    2481079}
    2491080
    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  
    33Donate link: http://hel.io/donate/
    44Tags: 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
     5Requires at least: 3.4
     6Tested up to: 3.4
     7Stable tag: 2.0
     8License: GPLv3
     9License URI: http://www.gnu.org/licenses/gpl.html
    810
    911Make backups of your Wordpress site to Google Drive.
     
    1113== Description ==
    1214
    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.
     15Version 2.0 is out and it's full of improvements and new features!
    1416
    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.
     17If 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
     19Backup 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
     21You are in total control of what files and directories get backed up.
    1622
    1723== Installation ==
    1824
     25The plugin requires WordPress 3.4 and is installed like any other plugin.
     26
    19271. Upload the plugin to the `/wp-contents/plugins/` folder.
    20282. Activate the plugin from the 'Plugins' menu in WordPress.
    21 3. Configure the plugin by following the instructions from the `Backup` settings page.
     293. Configure the plugin by following the instructions on the `Backup` settings page.
     30
     31If 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
     37I 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
     41Apart 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.
    2242
    2343== Screenshots ==
     
    2646
    2747== 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
    2865= 1.1.5 =
    2966* You can now chose not to upload backups to Google Drive by entering `0` in the appropriate field on the settings page.
     
    3168
    3269= 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.
    3471
    3572= 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.
    3976
    4077= 1.1.1 =
    41 * Fixed mothly backup frequency
     78* Fixed monthly backup frequency.
    4279
    4380= 1.1 =
    44 * Added abbility 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.
    4582* Added a page ( `/backup/` ) which can be used to trigger manual backups or used in cron jobs.
    46 * Added abbility to store a maximum of `n` backups.
     83* Added ability to store a maximum of `n` backups.
    4784* 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.
    5087
    5188= 1.0 =
Note: See TracChangeset for help on using the changeset viewer.