PluginProbe
UpdraftPlus: WP Backup & Migration Plugin / 0.9.1
UpdraftPlus: WP Backup & Migration Plugin v0.9.1
1.26.7 1.26.6 1.26.5 1.26.4 1.26.3 1.9.19 1.9.25 1.9.26 1.9.30 1.9.31 1.9.32 1.9.4 1.9.40 1.9.41 1.9.42 1.9.43 1.9.44 1.9.45 1.9.46 1.9.5 1.9.50 1.9.51 1.9.60 1.9.62 1.9.63 All 371 releases
updraftplus / updraftplus.php

updraftplus.php in UpdraftPlus: WP Backup & Migration Plugin 0.9.1, at updraftplus.php

2,383 lines 102.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 Plugin Name: UpdraftPlus - Backup/Restore
4 Plugin URI: http://wordpress.org/extend/plugins/updraftplus
5 Description: Uploads, themes, plugins, and your DB can be automatically backed up to Amazon S3, Google Drive, FTP, or emailed, on separate schedules.
6 Author: David Anderson.
7 Version: 0.9.1
8 Donate link: http://david.dw-perspective.org.uk/donate
9 Author URI: http://wordshell.net
10 */
11
12 //TODO (some of these items mine, some from original Updraft awaiting review):
13 //Add DropBox support
14 //Struggles with large uploads - runs out of time before finishing. Break into chunks? Resume download on later run? (Add a new scheduled event to check on progress? Separate the upload from the creation?).
15 //improve error reporting. s3 and dir backup have decent reporting now, but not sure i know what to do from here
16 //list backups that aren't tracked (helps with double backup problem)
17 //investigate $php_errormsg further
18 //pretty up return messages in admin area
19 //check s3/ftp download
20
21 //Rip out the "last backup" bit, and/or put in a display of the last log
22
23 /* More TODO:
24 DONE, TESTING: Are all directories in wp-content covered? No; only plugins, themes, content. We should check for others and allow the user the chance to choose which ones he wants
25 Use only one entry in WP options database
26 Encrypt filesystem, if memory allows (and have option for abort if not); split up into multiple zips when needed
27 // Does not delete old custom directories upon a restore?
28 */
29
30 /* Portions copyright 2010 Paul Kehrer
31 Portions copyright 2011-12 David Anderson
32
33 This program is free software; you can redistribute it and/or modify
34 it under the terms of the GNU General Public License as published by
35 the Free Software Foundation; either version 2 of the License, or
36 (at your option) any later version.
37
38 This program is distributed in the hope that it will be useful,
39 but WITHOUT ANY WARRANTY; without even the implied warranty of
40 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
41 GNU General Public License for more details.
42
43 You should have received a copy of the GNU General Public License
44 along with this program; if not, write to the Free Software
45 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
46 */
47 // TODO: Note this might *lower* the limit - should check first.
48
49 @set_time_limit(900); //15 minutes max. i'm not sure how long a really big site could take to back up?
50
51 $updraft = new UpdraftPlus();
52
53 if(!$updraft->memory_check(192)) {
54 # TODO: Better solution is to split the backup set into manageable chunks based on this limit
55 @ini_set('memory_limit', '192M'); //up the memory limit for large backup files... should split the backup set into manageable chunks based on the limit
56 }
57
58 define('UPDRAFT_DEFAULT_OTHERS_EXCLUDE','upgrade,cache,updraft,index.php');
59
60 class UpdraftPlus {
61
62 var $version = '0.9.1';
63
64 var $dbhandle;
65 var $errors = array();
66 var $nonce;
67 var $logfile_name = "";
68 var $logfile_handle = false;
69 var $backup_time;
70
71 function __construct() {
72 // Initialisation actions
73 # Create admin page
74 add_action('admin_menu', array($this,'add_admin_pages'));
75 add_action('admin_init', array($this,'admin_init'));
76 add_action('updraft_backup', array($this,'backup_files'));
77 add_action('updraft_backup_database', array($this,'backup_database'));
78 # backup_all is used by the manual "Backup Now" button
79 add_action('updraft_backup_all', array($this,'backup_all'));
80 # this is our runs-after-backup event, whose purpose is to see if it succeeded or failed, and resume/mom-up etc.
81 add_action('updraft_backup_resume', array($this,'backup_resume'));
82 add_action('wp_ajax_updraft_download_backup', array($this, 'updraft_download_backup'));
83 # http://codex.wordpress.org/Plugin_API/Filter_Reference/cron_schedules
84 add_filter('cron_schedules', array($this,'modify_cron_schedules'));
85 add_filter('plugin_action_links', array($this, 'plugin_action_links'), 10, 2);
86 add_action('init', array($this, 'googledrive_backup_auth'));
87 }
88
89 // Handle Google OAuth 2.0
90 function googledrive_backup_auth() {
91 if ( is_admin() && isset( $_GET['page'] ) && $_GET['page'] == 'updraftplus' && isset( $_GET['action'] ) && $_GET['action'] == 'auth' ) {
92 if ( isset( $_GET['state'] ) ) {
93 if ( $_GET['state'] == 'token' )
94 $this->auth_token();
95 elseif ( $_GET['state'] == 'revoke' )
96 $this->auth_revoke();
97 } elseif (isset($_GET['updraftplus_googleauth'])) {
98 $this->auth_request();
99 }
100 }
101 }
102
103 /**
104 * Acquire single-use authorization code from Google OAuth 2.0
105 */
106 function auth_request() {
107 $params = array(
108 'response_type' => 'code',
109 'client_id' => get_option('updraft_googledrive_clientid'),
110 'redirect_uri' => admin_url('options-general.php?page=updraftplus&action=auth'),
111 'scope' => 'https://www.googleapis.com/auth/drive.file https://docs.google.com/feeds/ https://docs.googleusercontent.com/ https://spreadsheets.google.com/feeds/',
112 'state' => 'token',
113 'access_type' => 'offline',
114 'approval_prompt' => 'auto'
115 );
116 header('Location: https://accounts.google.com/o/oauth2/auth?'.http_build_query($params));
117 }
118
119 /**
120 * Get a Google account access token using the refresh token
121 */
122 function access_token( $token, $client_id, $client_secret ) {
123 $context = array(
124 'http' => array(
125 'method' => 'POST',
126 'header' => 'Content-type: application/x-www-form-urlencoded',
127 'content' => http_build_query( array(
128 'refresh_token' => $token,
129 'client_id' => $client_id,
130 'client_secret' => $client_secret,
131 'grant_type' => 'refresh_token'
132 ) )
133 )
134 );
135 $this->log("Google Drive: requesting access token: client_id=$client_id");
136 $result = @file_get_contents('https://accounts.google.com/o/oauth2/token', false, stream_context_create($context));
137 if($result) {
138 $result = json_decode( $result, true );
139 if ( isset( $result['access_token'] ) ) {
140 $this->log("Google Drive: successfully obtained access token");
141 return $result['access_token'];
142 } else {
143 $this->log("Google Drive error when requesting access token: response does not contain access_token");
144 return false;
145 }
146 } else {
147 $this->log("Google Drive error when requesting access token: no response");
148 return false;
149 }
150 }
151
152 /**
153 * Function to upload a file to Google Drive
154 *
155 * @param string $file Path to the file that is to be uploaded
156 * @param string $title Title to be given to the file
157 * @param string $parent ID of the folder in which to upload the file
158 * @param string $token Access token from Google Account
159 * @return boolean Returns TRUE on success, FALSE on failure
160 */
161 function googledrive_upload_file( $file, $title, $parent = '', $token) {
162
163 $size = filesize( $file );
164
165 $content = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>
166 <entry xmlns="http://www.w3.org/2005/Atom" xmlns:docs="http://schemas.google.com/docs/2007">
167 <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/docs/2007#file"/>
168 <title>' . $title . '</title>
169 </entry>';
170
171 $header = array(
172 'Authorization: Bearer ' . $token,
173 'Content-Length: ' . strlen( $content ),
174 'Content-Type: application/atom+xml',
175 'X-Upload-Content-Type: application/octet-stream',
176 'X-Upload-Content-Length: ' . $size,
177 'GData-Version: 3.0'
178 );
179
180 $context = array(
181 'http' => array(
182 'ignore_errors' => true,
183 'follow_location' => false,
184 'method' => 'POST',
185 'header' => join( "\r\n", $header ),
186 'content' => $content
187 )
188 );
189
190 $url = $this->get_resumable_create_media_link( $token, $parent );
191 if ( $url ) {
192 $url .= '?convert=false'; // needed to upload a file
193 $this->log("Google Drive: resumable create media link: ".$url);
194 } else {
195 $this->log('Could not retrieve resumable create media link.', __FILE__, __LINE__ );
196 return false;
197 }
198
199 $result = @file_get_contents( $url, false, stream_context_create( $context ) );
200
201 if ( $result !== FALSE ) {
202 if ( strpos( $response = array_shift( $http_response_header ), '200' ) ) {
203 $response_header = array();
204 foreach ( $http_response_header as $header_line ) {
205 list( $key, $value ) = explode( ':', $header_line, 2 );
206 $response_header[trim( $key )] = trim( $value );
207 #$this->log("Google Drive: header: ".trim($key).": ".trim($value));
208 }
209 if ( isset( $response_header['Location'] ) ) {
210 $next_location = $response_header['Location'];
211 $pointer = 0;
212 # 1Mb
213 $max_chunk_size = 524288*2;
214 while ( $pointer < $size - 1 ) {
215 $this->log(basename($file).": Google Drive upload: pointer=$pointer (size=$size)");
216 $chunk = file_get_contents( $file, false, NULL, $pointer, $max_chunk_size );
217 $next_location = $this->upload_chunk( $next_location, $chunk, $pointer, $size, $token );
218 if( $next_location === false ) {
219 $this->log("Google Drive Upload: next_location is false (pointer: $pointer; chunk length: ".strlen($chunk).")");
220 return false;
221 }
222 $pointer += strlen( $chunk );
223 // if object it means we have our simpleXMLElement response
224 if ( is_object( $next_location ) ) {
225 // return resource Id
226 $this->log("Google Drive Upload: Success");
227 # Google Drive returns 501 not implemented for me for some reason instead of expected result...
228 #return substr( $next_location->children( "http://schemas.google.com/g/2005" )->resourceId, 5 );
229 return true;
230 }
231
232 }
233 }
234 }
235 else {
236 $this->log( 'Bad response: ' . $response . ' Response header: ' . var_export( $response_header, true ) . ' Response body: ' . $result . ' Request URL: ' . $url, __FILE__, __LINE__ );
237 return false;
238 }
239 }
240 else {
241 $this->log( 'Unable to request file from ' . $url, __FILE__, __LINE__ );
242 }
243
244 return true;
245
246 }
247
248 /**
249 * Get the resumable-create-media link needed to upload files
250 *
251 * @param string $token The Google Account access token
252 * @param string $parent The Id of the folder where the upload is to be made. Default is empty string.
253 * @return string|boolean Returns a link on success, FALSE on failure.
254 */
255 function get_resumable_create_media_link( $token, $parent = '' ) {
256 $header = array(
257 'Authorization: Bearer ' . $token,
258 'GData-Version: 3.0'
259 );
260 $context = array(
261 'http' => array(
262 'ignore_errors' => true,
263 'method' => 'GET',
264 'header' => join( "\r\n", $header )
265 )
266 );
267 $url = 'https://docs.google.com/feeds/default/private/full';
268
269 if ( $parent ) {
270 $url .= '/' . $parent;
271 }
272
273 $result = @file_get_contents( $url, false, stream_context_create( $context ) );
274
275 if ( $result !== false ) {
276 $xml = simplexml_load_string( $result );
277 if ( $xml === false ) {
278 $this->log( 'Could not create SimpleXMLElement from ' . $result, __FILE__, __LINE__ );
279 return false;
280 }
281 else {
282 foreach ( $xml->link as $link ) {
283 if ( $link['rel'] == 'http://schemas.google.com/g/2005#resumable-create-media' ) { return $link['href']; }
284 }
285 }
286 }
287 return false;
288 }
289
290
291 /**
292 * Handles the upload to Google Drive of a single chunk of a file
293 *
294 * @param string $location URL where the chunk needs to be uploaded
295 * @param string $chunk Part of the file to upload
296 * @param integer $pointer The byte number marking the beginning of the chunk in file
297 * @param integer $size The size of the file the chunk is part of, in bytes
298 * @param string $token Google Account access token
299 * @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
300 */
301 function upload_chunk( $location, $chunk, $pointer, $size, $token ) {
302 $chunk_size = strlen( $chunk );
303 $bytes = (string)$pointer . '-' . (string)($pointer + $chunk_size - 1) . '/' . (string)$size;
304 $this->log("Google Drive chunk: location=$location, length=$chunk_size, range=$bytes");
305 $header = array(
306 'Authorization: Bearer ' . $token,
307 'Content-Length: ' . $chunk_size,
308 'Content-Type: application/octet-stream',
309 'Content-Range: bytes ' . $bytes,
310 'GData-Version: 3.0'
311 );
312 $context = array(
313 'http' => array(
314 'ignore_errors' => true,
315 'follow_location' => false,
316 'method' => 'PUT',
317 'header' => join( "\r\n", $header ),
318 'content' => $chunk
319 )
320 );
321
322 $result = @file_get_contents( $location, false, stream_context_create( $context ) );
323
324 if ( isset( $http_response_header ) ) {
325 $response = array_shift( $http_response_header );
326 $headers = array();
327 foreach ( $http_response_header as $header_line ) {
328 list( $key, $value ) = explode( ':', $header_line, 2 );
329 $headers[trim( $key )] = trim( $value );
330 }
331
332 if ( strpos( $response, '308' ) ) {
333 if ( isset( $headers['Location'] ) ) {
334 $this->log('Google Drive: 308 response: '.$headers['Location']);
335 return $headers['Location'];
336 }
337 else {
338 $this->log('Google Drive 308 response: no location header: '.$location);
339 return $location;
340 }
341 }
342 elseif ( strpos( $response, '201' ) ) {
343 #$this->log("Google Drive response: ".$result);
344 $xml = simplexml_load_string( $result );
345 if ( $xml === false ) {
346 $this->log('ERROR: Could not create SimpleXMLElement from ' . $result, __FILE__, __LINE__ );
347 return false;
348 }
349 else {
350 return $xml;
351 }
352 }
353 else {
354 $this->log('ERROR: Bad response: ' . $response, __FILE__, __LINE__ );
355 return false;
356 }
357 }
358 else {
359 $this->log('ERROR: Received no response from ' . $location . ' while trying to upload bytes ' . $bytes );
360 return false;
361 }
362 }
363
364 function googledrive_delete_file( $file, $token) {
365 $this->log("Delete from Google Drive: $file: not yet implemented");
366 # TODO - somehow, turn this into a Gdata resource ID, then despatch it to googledrive_delete_file_byid
367 return;
368 }
369
370 /**
371 * Deletes a file from Google Drive
372 *
373 * @param string $id Gdata resource Id of the file to be deleted
374 * @param string $token Google Account access token
375 * @return boolean Returns TRUE on success, FALSE on failure
376 */
377 function googledrive_delete_file_byid( $id, $token ) {
378 $header = array(
379 'If-Match: *',
380 'Authorization: Bearer ' . $token,
381 'GData-Version: 3.0'
382 );
383 $context = array(
384 'http' => array(
385 'method' => 'DELETE',
386 'header' => join( "\r\n", $header )
387 )
388 );
389 stream_context_set_default( $context );
390 $headers = get_headers( 'https://docs.google.com/feeds/default/private/full/' . $id . '?delete=true',1 );
391
392 if ( strpos( $headers[0], '200' ) ) { return true; }
393 return false;
394 }
395
396 /**
397 * Get a Google account refresh token using the code received from auth_request
398 */
399 function auth_token() {
400 if( isset( $_GET['code'] ) ) {
401 $context = array(
402 'http' => array(
403 'timeout' => 30,
404 'method' => 'POST',
405 'header' => 'Content-type: application/x-www-form-urlencoded',
406 'content' => http_build_query( array(
407 'code' => $_GET['code'],
408 'client_id' => get_option('updraft_googledrive_clientid'),
409 'client_secret' => get_option('updraft_googledrive_secret'),
410 'redirect_uri' => admin_url('options-general.php?page=updraftplus&action=auth'),
411 'grant_type' => 'authorization_code'
412 ) )
413 )
414 );
415 $result = @file_get_contents('https://accounts.google.com/o/oauth2/token', false, stream_context_create($context));
416 # Oddly, sometimes fails and then trying again works...
417 /*
418 if (!$result) { sleep(1); $result = @file_get_contents('https://accounts.google.com/o/oauth2/token', false, stream_context_create($context));}
419 if (!$result) { sleep(1); $result = @file_get_contents('https://accounts.google.com/o/oauth2/token', false, stream_context_create($context));}
420 */
421 if($result) {
422 $result = json_decode( $result, true );
423 if ( isset( $result['refresh_token'] ) ) {
424 update_option('updraft_googledrive_token',$result['refresh_token']); // Save token
425 header('Location: '.admin_url('options-general.php?page=updraftplus&message=' . __( 'Authorization was successful.', 'updraftplus' ) ) );
426 }
427 else {
428 header('Location: '.admin_url('options-general.php?page=updraftplus&error=' . __( 'No refresh token was received!', 'updraftplus' ) ) );
429 }
430 } else {
431 header('Location: '.admin_url('options-general.php?page=updraftplus&error=' . __( 'Bad response!', 'backup' ) ) );
432 }
433 }
434 else {
435 header('Location: '.admin_url('options-general.php?page=updraftplus&error=' . __( 'Authorisation failed!', 'backup' ) ) );
436 }
437 }
438
439 /**
440 * Revoke a Google account refresh token
441 */
442 function auth_revoke() {
443 @file_get_contents( 'https://accounts.google.com/o/oauth2/revoke?token=' . get_option('updraft_googledrive_token') );
444 update_option('updraft_googledrive_token','');
445 header( 'Location: '.admin_url( 'options-general.php?page=updraftplus&message=' . __( 'Authorization revoked.', 'backup' ) ) );
446 }
447
448 # Adds the settings link under the plugin on the plugin screen.
449 function plugin_action_links($links, $file) {
450 if ($file == plugin_basename(__FILE__)){
451 $settings_link = '<a href="'.site_url().'/wp-admin/options-general.php?page=updraftplus">'.__("Settings", "UpdraftPlus").'</a>';
452 array_unshift($links, $settings_link);
453 $settings_link = '<a href="http://david.dw-perspective.org.uk/donate">'.__("Donate","UpdraftPlus").'</a>';
454 array_unshift($links, $settings_link);
455 }
456 return $links;
457 }
458
459 function backup_time_nonce() {
460 $this->backup_time = time();
461 $this->nonce = substr(md5(time().rand()),20);
462 }
463
464 # Logs the given line, adding date stamp and newline
465 function log($line) {
466 if ($this->logfile_handle) fwrite($this->logfile_handle,date('r')." ".$line."\n");
467 }
468
469 function backup_resume($resumption_no) {
470 // This is scheduled for 5 minutes after a backup job starts
471 $bnonce = get_transient('updraftplus_backup_job_nonce');
472 if (!$bnonce) return;
473 $this->nonce = $bnonce;
474 $this->logfile_open($bnonce);
475 $this->log("Resume backup ($resumption_no): begin run (will check for any remaining jobs)");
476 $btime = get_transient('updraftplus_backup_job_time');
477 if (!$btime) {
478 $this->log("Did not find stored time setting - aborting");
479 return;
480 }
481 $this->log("Resuming backup: resumption=$resumption_no, nonce=$bnonce, begun at=$btime");
482 // Schedule again, to run in 5 minutes again, in case we again fail
483 $resume_delay = 300;
484 // A different argument than before is needed otherwise the event is ignored
485 $next_resumption = $resumption_no+1;
486 if ($next_resumption < 10) {
487 wp_schedule_single_event(time()+$resume_delay, 'updraft_backup_resume' ,array($next_resumption));
488 } else {
489 $this->log("This is our tenth attempt - will not try again");
490 }
491 $this->backup_time = $btime;
492
493 // Returns an array, most recent first, of backup sets
494 $backup_history = $this->get_backup_history();
495 if (!isset($backup_history[$btime])) $this->log("Error: Could not find a record in the database of a backup with this timestamp");
496
497 $our_files=$backup_history[$btime];
498 $undone_files = array();
499 foreach ($our_files as $key => $file) {
500 $hash=md5($file);
501 $fullpath = trailingslashit(get_option('updraft_dir')).$file;
502 if (get_transient('updraft_'.$hash) === "yes") {
503 $this->log("$file: $key: This file has been successfully uploaded in the last 3 hours");
504 } elseif (is_file($fullpath)) {
505 $this->log("$file: $key: This file has NOT been successfully uploaded in the last 3 hours: will retry");
506 $undone_files[$key] = $file;
507 } else {
508 $this-log("$file: Note: This file was not marked as successfully uploaded, but does not exist on the local filesystem");
509 $this->uploaded_file($file);
510 }
511 }
512
513 if (count($undone_files) == 0) {
514 $this->log("There were no files that needed uploading; backup job is finished");
515 return;
516 }
517
518 $this->log("Requesting backup of the files that were not successfully uploaded");
519 $this->cloud_backup($undone_files);
520 $this->cloud_backup_finish($undone_files);
521
522 $this->log("Resume backup ($resumption_no): finish run");
523
524 $this->backup_finish($next_resumption);
525
526 }
527
528 function backup_all() {
529 $this->backup(true,true);
530 }
531
532 function backup_files() {
533 # Note that the "false" for database gets over-ridden automatically if they turn out to have the same schedules
534 $this->backup(true,false);
535 }
536
537 function backup_database() {
538 # Note that nothing will happen if the file backup had the same schedule
539 $this->backup(false,true);
540 }
541
542 function logfile_open($nonce) {
543 //set log file name and open log file
544 $updraft_dir = $this->backups_dir_location();
545 $this->logfile_name = $updraft_dir. "/log.$nonce.txt";
546 // Use append mode in case it already exists
547 $this->logfile_handle = fopen($this->logfile_name, 'a');
548 }
549
550 //scheduled wp-cron events can have a race condition here if page loads are coming fast enough, but there's nothing we can do about it. TODO: I reckon there is. Store a transient based on the backup schedule. Then as the backup proceeds, check for its existence; if it has changed, then another task has begun, so abort.
551 function backup($backup_files, $backup_database) {
552
553 //generate backup information
554 $this->backup_time_nonce();
555 // If we don't finish in 3 hours, then we won't finish
556 // This transient indicates the identity of the current backup job (which can be used to find the files and logfile)
557 set_transient("updraftplus_backup_job_nonce",$this->nonce,3600*3);
558 set_transient("updraftplus_backup_job_time",$this->backup_time,3600*3);
559 $this->logfile_open($this->nonce);
560
561 // Schedule the even to run later, which checks on success and can resume the backup
562 // We save the time to a variable because it is needed for un-scheduling
563 // $resume_delay = (get_option('updraft_debug_mode')) ? 60 : 300;
564 $resume_delay = 300;
565 wp_schedule_single_event(time()+$resume_delay, 'updraft_backup_resume', array(1));
566 $this->log("In case we run out of time, scheduled a resumption at: $resume_delay seconds from now");
567
568 // Log some information that may be helpful
569 global $wp_version;
570 $this->log("PHP version: ".phpversion()." WordPress version: ".$wp_version." Updraft version: ".$this->version." Backup files: $backup_files (schedule: ".get_option('updraft_interval','unset').") Backup DB: $backup_database (schedule: ".get_option('updraft_interval_database','unset').")");
571
572 # If the files and database schedules are the same, and if this the file one, then we rope in database too.
573 # On the other hand, if the schedules were the same and this was the database run, then there is nothing to do.
574 if (get_option('updraft_interval') == get_option('updraft_interval_database') || get_option('updraft_interval_database','xyz') == 'xyz' ) {
575 $backup_database = ($backup_files == true) ? true : false;
576 }
577
578 $this->log("Processed schedules. Tasks now: Backup files: $backup_files Backup DB: $backup_database");
579
580 # Possibly now nothing is to be done, except to close the log file
581 if ($backup_files || $backup_database) {
582
583 $backup_contains = "";
584
585 $backup_array = array();
586
587 //backup directories and return a numerically indexed array of file paths to the backup files
588 if ($backup_files) {
589 $this->log("Beginning backup of directories");
590 $backup_array = $this->backup_dirs();
591 $backup_contains = "Files only (no database)";
592 }
593
594 //backup DB and return string of file path
595 if ($backup_database) {
596 $this->log("Beginning backup of database");
597 $db_backup = $this->backup_db();
598 //add db path to rest of files
599 if(is_array($backup_array)) { $backup_array['db'] = $db_backup; }
600 $backup_contains = ($backup_files) ? "Files and database" : "Database only (no files)";
601 }
602
603 set_transient("updraftplus_backupcontains", $backup_contains, 3600*3);
604
605 //save this to our history so we can track backups for the retain feature
606 $this->log("Saving backup history");
607 // This is done before cloud despatch, because we want a record of what *should* be in the backup. Whether it actually makes it there or not is not yet known.
608 $this->save_backup_history($backup_array);
609
610 //cloud operations (S3,Google Drive,FTP,email,nothing)
611 //this also calls the retain (prune) feature at the end (done in this method to reuse existing cloud connections)
612 if(is_array($backup_array) && count($backup_array) >0) {
613 $this->log("Beginning dispatch of backup to remote");
614 $this->cloud_backup($backup_array);
615 }
616
617 //save the last backup info, including errors, if any
618 $this->log("Saving last backup information into WordPress db");
619 $this->save_last_backup($backup_array);
620
621 // Delete local files, send the email
622 $this->cloud_backup_finish($backup_array);
623
624 }
625
626 // Close log file; delete and also delete transients if not in debug mode
627 $this->backup_finish(1);
628
629 }
630
631 function backup_finish($cancel_event) {
632
633 // In fact, leaving the hook to run (if debug is set) is harmless, as the resume job should only do tasks that were left unfinished, which at this stage is none.
634 if (empty($this->errors)) {
635 $this->log("There were no errors in the uploads, so the 'resume' event is being unscheduled");
636 wp_clear_scheduled_hook('updraft_backup_resume', array($cancel_event));
637 delete_transient("updraftplus_backup_job_nonce");
638 delete_transient("updraftplus_backup_job_time");
639 } else {
640 $this->log("There were errors in the uploads, so the 'resume' event is remaining unscheduled");
641 }
642
643 @fclose($this->logfile_handle);
644
645 if (!get_option('updraft_debug_mode')) @unlink($this->logfile_name);
646
647 }
648
649 function cloud_backup_finish($backup_array) {
650
651 //delete local files if the pref is set
652 foreach($backup_array as $file) { $this->delete_local($file); }
653
654 // Send the results email if requested
655 if(get_option('updraft_email') != "" && get_option('updraft_service') != 'email') $this->send_results_email();
656
657 }
658
659
660 function send_results_email() {
661
662 $sendmail_to = get_option('updraft_email');
663
664 $this->log("Sending email report to: ".$sendmail_to);
665
666 $append_log = (get_option('updraft_debug_mode') && $this->logfile_name != "") ? "\r\nLog contents:\r\n".file_get_contents($this->logfile_name) : "" ;
667
668 wp_mail($sendmail_to,'Backed up: '.get_bloginfo('name').' (UpdraftPlus) '.date('Y-m-d H:i',time()),'Site: '.site_url()."\r\nUpdraftPlus WordPress backup is complete.\r\nBackup contains: ".get_transient("updraftplus_backupcontains")."\r\n\r\n".$this->wordshell_random_advert(0)."\r\n".$append_log);
669
670 }
671
672 function save_last_backup($backup_array) {
673 $success = (empty($this->errors))?1:0;
674
675 $last_backup = array('backup_time'=>$this->backup_time, 'backup_array'=>$backup_array, 'success'=>$success, 'errors'=>$this->errors);
676
677 update_option('updraft_last_backup', $last_backup);
678 }
679
680 // This should be called whenever a file is successfully uploaded
681 function uploaded_file($file) {
682 # We take an MD5 hash because set_transient wants a name of 45 characters or less
683 $hash = md5($file);
684 set_transient("updraft_".$hash, "yes", 3600*3);
685 }
686
687 function cloud_backup($backup_array) {
688 switch(get_option('updraft_service')) {
689 case 's3':
690 @set_time_limit(900);
691 $this->log("Cloud backup: S3");
692 if (count($backup_array) >0) $this->s3_backup($backup_array);
693 break;
694 case 'googledrive':
695 @set_time_limit(900);
696 $this->log("Cloud backup: Google Drive");
697 if (count($backup_array) >0) $this->googledrive_backup($backup_array);
698 break;
699 case 'ftp':
700 @set_time_limit(900);
701 $this->log("Cloud backup: FTP");
702 if (count($backup_array) >0) $this->ftp_backup($backup_array);
703 break;
704 case 'email':
705 @set_time_limit(900);
706 $this->log("Cloud backup: Email");
707 //files can easily get way too big for this...
708 foreach($backup_array as $type=>$file) {
709 $fullpath = trailingslashit(get_option('updraft_dir')).$file;
710 wp_mail(get_option('updraft_email'),"WordPress Backup ".date('Y-m-d H:i',$this->backup_time),"Backup is of the $type. Be wary; email backups may fail because of file size limitations on mail servers.",null,array($fullpath));
711 $this->uploaded_file($file);
712 }
713 //we don't break here so it goes and executes all the default behavior below as well. this gives us retain behavior for email
714 default:
715 $this->prune_retained_backups("local");
716 break;
717 }
718 }
719
720 // Carries out retain behaviour. Pass in a valid S3 or FTP object and path if relevant.
721 function prune_retained_backups($updraft_service,$remote_object,$remote_path) {
722 $this->log("Retain: beginning examination of existing backup sets");
723 $updraft_retain = get_option('updraft_retain');
724 // Number of backups to retain
725 $retain = (isset($updraft_retain))?get_option('updraft_retain'):1;
726 $this->log("Retain: user setting: number to retain = $retain");
727 // Returns an array, most recent first, of backup sets
728 $backup_history = $this->get_backup_history();
729 $db_backups_found = 0;
730 $file_backups_found = 0;
731 $this->log("Number of backup sets in history: ".count($backup_history));
732 foreach ($backup_history as $backup_datestamp => $backup_to_examine) {
733 // $backup_to_examine is an array of file names, keyed on db/plugins/themes/uploads
734 // The new backup_history array is saved afterwards, so remember to unset the ones that are to be deleted
735 $this->log("Examining backup set with datestamp: $backup_datestamp");
736 if (isset($backup_to_examine['db'])) {
737 $db_backups_found++;
738 $this->log("$backup_datestamp: this set includes a database (".$backup_to_examine['db']."); db count is now $db_backups_found");
739 if ($db_backups_found > $retain) {
740 $this->log("$backup_datestamp: over retain limit; will delete this database");
741 $file = $backup_to_examine['db'];
742 $this->log("$backup_datestamp: Delete this file: $file");
743 if ($file != '') {
744 $fullpath = trailingslashit(get_option('updraft_dir')).$file;
745 @unlink($fullpath); //delete it if it's locally available
746 if ($updraft_service == "s3") {
747 if (preg_match("#^([^/]+)/(.*)$#",$remote_path,$bmatches)) {
748 $s3_bucket=$bmatches[1];
749 $s3_uri = $bmatches[2]."/".$file;
750 } else {
751 $s3_bucket = $remote_path;
752 $s3_uri = $file;
753 }
754 $this->log("$backup_datestamp: Delete remote: bucket=$s3_bucket, URI=$s3_uri");
755 # Here we brought in the function deleteObject in order to get more direct access to any error
756 $rest = new S3Request('DELETE', $s3_bucket, $s3_uri);
757 $rest = $rest->getResponse();
758 if ($rest->error === false && $rest->code !== 204) {
759 $this->log("S3 Error: Expected HTTP response 204; got: ".$rest->code);
760 $this->error("S3 Error: Unexpected HTTP response code ".$rest->code." (expected 204)");
761 } elseif ($rest->error !== false) {
762 $this->log("S3 Error: ".$rest->error['code'].": ".$rest->error['message']);
763 $this->error("S3 delete error: ".$rest->error['code'].": ".$rest->error['message']);
764 }
765 } elseif ($updraft_service == "ftp") {
766 $this->log("$backup_datestamp: Delete remote ftp: $remote_path/$file");
767 @$remote_object->delete($remote_path.$file);
768 } elseif ($updraft_service == "googledrive") {
769 $this->log("$backup_datestamp: Delete remote file from Google Drive: $remote_path/$file");
770 $this->googledrive_delete_file($remote_path.'/'.$file,$remote_object);
771 }
772 }
773 unset($backup_to_examine['db']);
774 }
775 }
776 if (isset($backup_to_examine['plugins']) || isset($backup_to_examine['themes']) || isset($backup_to_examine['uploads']) || isset($backup_to_examine['others'])) {
777 $file_backups_found++;
778 $this->log("$backup_datestamp: this set includes files; fileset count is now $file_backups_found");
779 if ($file_backups_found > $retain) {
780 $this->log("$backup_datestamp: over retain limit; will delete this file set");
781 $file = isset($backup_to_examine['plugins']) ? $backup_to_examine['plugins'] : "";
782 $file2 = isset($backup_to_examine['themes']) ? $backup_to_examine['themes'] : "";
783 $file3 = isset($backup_to_examine['uploads']) ? $backup_to_examine['uploads'] : "";
784 $file4 = isset($backup_to_examine['others']) ? $backup_to_examine['others'] : "";
785 foreach (array($file,$file2,$file3,$file4) as $dofile) {
786 if ($dofile) {
787 $this->log("$backup_datestamp: Delete this file: $dofile");
788 $fullpath = trailingslashit(get_option('updraft_dir')).$dofile;
789 @unlink($fullpath); //delete it if it's locally available
790 if ($updraft_service == "s3") {
791 if (preg_match("#^([^/]+)/(.*)$#",$remote_path,$bmatches)) {
792 $s3_bucket=$bmatches[1];
793 $s3_uri = $bmatches[2]."/".$dofile;
794 } else {
795 $s3_bucket = $remote_path;
796 $s3_uri = $dofile;
797 }
798 $this->log("$backup_datestamp: Delete remote: bucket=$s3_bucket, URI=$s3_uri");
799 # Here we brought in the function deleteObject in order to get more direct access to any error
800 $rest = new S3Request('DELETE', $s3_bucket, $s3_uri);
801 $rest = $rest->getResponse();
802 if ($rest->error === false && $rest->code !== 204) {
803 $this->log("S3 Error: Expected HTTP response 204; got: ".$rest->code);
804 $this->error("S3 Error: Unexpected HTTP response code ".$rest->code." (expected 204)");
805 } elseif ($rest->error !== false) {
806 $this->log("S3 Error: ".$rest->error['code'].": ".$rest->error['message']);
807 $this->error("S3 delete error: ".$rest->error['code'].": ".$rest->error['message']);
808 }
809 } elseif ($updraft_service == "ftp") {
810 $this->log("$backup_datestamp: Delete remote ftp: $remote_path/$dofile");
811 @$remote_object->delete($remote_path.$dofile);
812 } elseif ($updraft_service == "googledrive") {
813 $this->log("$backup_datestamp: Delete remote file from Google Drive: $remote_path/$dofile");
814 $this->googledrive_delete_file($remote_path.'/'.$dofile,$remote_object);
815 }
816 }
817 }
818 unset($backup_to_examine['plugins']);
819 unset($backup_to_examine['themes']);
820 unset($backup_to_examine['uploads']);
821 unset($backup_to_examine['others']);
822 }
823 }
824 // Delete backup set completely if empty, o/w just remove DB
825 if (count($backup_to_examine)==0) {
826 $this->log("$backup_datestamp: this backup set is now empty; will remove from history");
827 unset($backup_history[$backup_datestamp]);
828 } else {
829 $this->log("$backup_datestamp: this backup set remains non-empty; will retain in history");
830 $backup_history[$backup_datestamp] = $backup_to_examine;
831 }
832 }
833 $this->log("Retain: saving new backup history (sets now: ".count($backup_history).") and finishing retain operation");
834 update_option('updraft_backup_history',$backup_history);
835 }
836
837 function s3_backup($backup_array) {
838 if(!class_exists('S3')) require_once(dirname(__FILE__).'/includes/S3.php');
839 $s3 = new S3(get_option('updraft_s3_login'), get_option('updraft_s3_pass'));
840 $bucket_name = untrailingslashit(get_option('updraft_s3_remote_path'));
841 $bucket_path = "";
842 $orig_bucket_name = $bucket_name;
843 if (preg_match("#^([^/]+)/(.*)$#",$bucket_name,$bmatches)) {
844 $bucket_name = $bmatches[1];
845 $bucket_path = $bmatches[2]."/";
846 }
847 if (@$s3->putBucket($bucket_name, S3::ACL_PRIVATE)) {
848 foreach($backup_array as $file) {
849 $fullpath = trailingslashit(get_option('updraft_dir')).$file;
850 $this->log("S3 upload: $fullpath -> s3://$bucket_name/$bucket_path$file");
851 if (!$s3->putObjectFile($fullpath, $bucket_name, $bucket_path.$file)) {
852 $this->log("S3 upload: failed");
853 $this->error("S3 Error: Failed to upload $fullpath. Error was ".$php_errormsg);
854 } else {
855 $this->log("S3 upload: success");
856 $this->uploaded_file($file);
857 }
858 }
859 $this->prune_retained_backups('s3',$s3,$orig_bucket_name);
860 } else {
861 $this->log("S3 Error: Failed to create bucket $bucket_name. Error was ".$php_errormsg);
862 $this->error("S3 Error: Failed to create bucket $bucket_name. Error was ".$php_errormsg);
863 }
864 }
865
866 function googledrive_backup($backup_array) {
867 if ( $access = $this->access_token( get_option('updraft_googledrive_token'), get_option('updraft_googledrive_clientid'), get_option('updraft_googledrive_secret') ) ) {
868 foreach ($backup_array as $file) {
869 $file_path = trailingslashit(get_option('updraft_dir')).$file;
870 $file_name = basename($file_path);
871 $this->log("$file_name: Attempting to upload to Google Drive");
872 $timer_start = microtime( true );
873 if ( $id = $this->googledrive_upload_file( $file_path, $file_name, get_option('updraft_googledrive_remotepath'), $access ) ) {
874 $this->log('OK: Archive ' . $file_name . ' uploaded to Google Drive in ' . ( round(microtime( true ) - $timer_start,2) ) . ' seconds' );
875 $this->uploaded_file($file);
876 } else {
877 $this->error("$file_name: Failed to upload to Google Drive" );
878 $this->log("ERROR: $file_name: Failed to upload to Google Drive" );
879 }
880 }
881 $this->prune_retained_backups("googledrive",$access,get_option('updraft_googledrive_remotepath'));
882 } else {
883 $this->log('ERROR: Did not receive an access token from Google', __FILE__, __LINE__ );
884 }
885 }
886
887 function ftp_backup($backup_array) {
888 if( !class_exists('ftp_wrapper')) {
889 require_once(dirname(__FILE__).'/includes/ftp.class.php');
890 }
891 //handle SSL and errors at some point TODO
892 $ftp = new ftp_wrapper(get_option('updraft_server_address'),get_option('updraft_ftp_login'),get_option('updraft_ftp_pass'));
893 $ftp->passive = true;
894 $ftp->connect();
895 //$ftp->make_dir(); we may need to recursively create dirs? TODO
896
897 $ftp_remote_path = trailingslashit(get_option('updraft_ftp_remote_path'));
898 foreach($backup_array as $file) {
899 $fullpath = trailingslashit(get_option('updraft_dir')).$file;
900 if ($ftp->put($fullpath,$ftp_remote_path.$file,FTP_BINARY)) {
901 $this->log("ERROR: $file_name: Successfully uploaded via FTP");
902 $this->uploaded_file($file);
903 } else {
904 $this->error("$file_name: Failed to upload to FTP" );
905 $this->log("ERROR: $file_name: Failed to upload to FTP" );
906 }
907 }
908 $this->prune_retained_backups("ftp",$ftp,$ftp_remote_path);
909 }
910
911 function delete_local($file) {
912 if(get_option('updraft_delete_local')) {
913 $this->log("Deleting local file: $file");
914 //need error checking so we don't delete what isn't successfully uploaded?
915 $fullpath = trailingslashit(get_option('updraft_dir')).$file;
916 return unlink($fullpath);
917 }
918 return true;
919 }
920
921 function backup_dirs() {
922 if(!$this->backup_time) {
923 $this->backup_time_nonce();
924 }
925 $wp_themes_dir = WP_CONTENT_DIR.'/themes';
926 $wp_upload_dir = wp_upload_dir();
927 $wp_upload_dir = $wp_upload_dir['basedir'];
928 $wp_plugins_dir = WP_PLUGIN_DIR;
929
930 if(!class_exists('PclZip')) { require_once(ABSPATH.'/wp-admin/includes/class-pclzip.php'); }
931
932 $updraft_dir = $this->backups_dir_location();
933 if(!is_writable($updraft_dir)) {
934 $this->error('Backup directory is not writable, or does not exist.','fatal');
935 }
936 //get the blog name and rip out all non-alphanumeric chars other than _
937 $blog_name = str_replace(' ','_',get_bloginfo());
938 $blog_name = preg_replace('/[^A-Za-z0-9_]/','', $blog_name);
939 if(!$blog_name) {
940 $blog_name = 'non_alpha_name';
941 }
942
943 $backup_file_base = $updraft_dir.'/backup_'.date('Y-m-d-Hi',$this->backup_time).'_'.$blog_name.'_'.$this->nonce;
944
945 $backup_array = array();
946
947 # Plugins
948 @set_time_limit(900);
949 if (get_option('updraft_include_plugins', true)) {
950 $this->log("Beginning backup of plugins");
951 $plugins = new PclZip($backup_file_base.'-plugins.zip');
952 # The paths in the zip should then begin with 'plugins', having removed WP_CONTENT_DIR from the front
953 if (!$plugins->create($wp_plugins_dir,PCLZIP_OPT_REMOVE_PATH,WP_CONTENT_DIR)) {
954 $this->error('Could not create plugins zip. Error was '.$php_errmsg,'fatal');
955 $this->log('ERROR: PclZip failure: Could not create plugins zip');
956 }
957 $backup_array['plugins'] = basename($backup_file_base.'-plugins.zip');
958 } else {
959 $this->log("No backup of plugins: excluded by user's options");
960 }
961
962 # Themes
963 @set_time_limit(900);
964 if (get_option('updraft_include_themes', true)) {
965 $this->log("Beginning backup of themes");
966 $themes = new PclZip($backup_file_base.'-themes.zip');
967 if (!$themes->create($wp_themes_dir,PCLZIP_OPT_REMOVE_PATH,WP_CONTENT_DIR)) {
968 $this->error('Could not create themes zip. Error was '.$php_errmsg,'fatal');
969 $this->log('ERROR: PclZip failure: Could not create themes zip');
970 }
971 $backup_array['themes'] = basename($backup_file_base.'-themes.zip');
972 } else {
973 $this->log("No backup of themes: excluded by user's options");
974 }
975
976 # Uploads
977 @set_time_limit(900);
978 if (get_option('updraft_include_uploads', true)) {
979 $this->log("Beginning backup of uploads");
980 $uploads = new PclZip($backup_file_base.'-uploads.zip');
981 if (!$uploads->create($wp_upload_dir,PCLZIP_OPT_REMOVE_PATH,WP_CONTENT_DIR)) {
982 $this->error('Could not create uploads zip. Error was '.$php_errmsg,'fatal');
983 $this->log('ERROR: PclZip failure: Could not create uploads zip');
984 }
985 $backup_array['uploads'] = basename($backup_file_base.'-uploads.zip');
986 } else {
987 $this->log("No backup of uploads: excluded by user's options");
988 }
989
990 # Others
991 @set_time_limit(900);
992 if (get_option('updraft_include_others', true)) {
993 $this->log("Beginning backup of other directories found in the content directory");
994 $others = new PclZip($backup_file_base.'-others.zip');
995 // http://www.phpconcept.net/pclzip/user-guide/53
996 /* First parameter to create is:
997 An array of filenames or dirnames,
998 or
999 A string containing the filename or a dirname,
1000 or
1001 A string containing a list of filename or dirname separated by a comma.
1002 */
1003 // First, see what we can find. We always want to exclude these:
1004 $wp_themes_dir = WP_CONTENT_DIR.'/themes';
1005 $wp_upload_dir = wp_upload_dir();
1006 $wp_upload_dir = $wp_upload_dir['basedir'];
1007 $wp_plugins_dir = WP_PLUGIN_DIR;
1008 $updraft_dir = untrailingslashit(get_option('updraft_dir'));
1009
1010 # Initialise
1011 $other_dirlist = array();
1012
1013 $others_skip = preg_split("/,/",get_option('updraft_include_others_exclude',UPDRAFT_DEFAULT_OTHERS_EXCLUDE));
1014 # Make the values into the keys
1015 $others_skip = array_flip($others_skip);
1016
1017 $this->log('Looking for candidates to back up in: '.WP_CONTENT_DIR);
1018 if ($handle = opendir(WP_CONTENT_DIR)) {
1019 while (false !== ($entry = readdir($handle))) {
1020 $candidate = WP_CONTENT_DIR.'/'.$entry;
1021 if ($entry == "." || $entry == "..") { ; }
1022 elseif ($candidate == $updraft_dir) { $this->log("$entry: skipping: this is the updraft directory"); }
1023 elseif ($candidate == $wp_themes_dir) { $this->log("$entry: skipping: this is the themes directory"); }
1024 elseif ($candidate == $wp_upload_dir) { $this->log("$entry: skipping: this is the uploads directory"); }
1025 elseif ($candidate == $wp_plugins_dir) { $this->log("$entry: skipping: this is the plugins directory"); }
1026 elseif (isset($others_skip[$entry])) { $this->log("$entry: skipping: excluded by options"); }
1027 else { $this->log("$entry: adding to list"); array_push($other_dirlist,$candidate); }
1028 }
1029 } else {
1030 $this->log('ERROR: Could not read the content directory: '.WP_CONTENT_DIR);
1031 }
1032
1033 if (count($other_dirlist)>0) {
1034 if (!$others->create($other_dirlist,PCLZIP_OPT_REMOVE_PATH,WP_CONTENT_DIR)) {
1035 $this->error('Could not create other zip. Error was '.$php_errmsg,'fatal');
1036 $this->log('ERROR: PclZip failure: Could not create other zip');
1037 }
1038 $backup_array['others'] = basename($backup_file_base.'-others.zip');
1039 } else {
1040 $this->log("No backup of other directories: there was nothing found to back up");
1041 }
1042 } else {
1043 $this->log("No backup of other directories: excluded by user's options");
1044 }
1045 return $backup_array;
1046 }
1047
1048 function save_backup_history($backup_array) {
1049 //TODO: this stores full paths right now. should probably concatenate with ABSPATH to make it easier to move sites
1050 if(is_array($backup_array)) {
1051 $backup_history = get_option('updraft_backup_history');
1052 $backup_history = (is_array($backup_history)) ? $backup_history : array();
1053 $backup_history[$this->backup_time] = $backup_array;
1054 update_option('updraft_backup_history',$backup_history);
1055 } else {
1056 $this->error('Could not save backup history because we have no backup array. Backup probably failed.');
1057 }
1058 }
1059
1060 function get_backup_history() {
1061 //$backup_history = get_option('updraft_backup_history');
1062 //by doing a raw DB query to get the most up-to-date data from this option we slightly narrow the window for the multiple-cron race condition
1063 global $wpdb;
1064 $backup_history = @unserialize($wpdb->get_var($wpdb->prepare("SELECT option_value from $wpdb->options WHERE option_name='updraft_backup_history'")));
1065 if(is_array($backup_history)) {
1066 krsort($backup_history); //reverse sort so earliest backup is last on the array. this way we can array_pop
1067 } else {
1068 $backup_history = array();
1069 }
1070 return $backup_history;
1071 }
1072
1073
1074 /*START OF WB-DB-BACKUP BLOCK*/
1075
1076 function backup_db() {
1077
1078 $total_tables = 0;
1079
1080 global $table_prefix, $wpdb;
1081 if(!$this->backup_time) {
1082 $this->backup_time_nonce();
1083 }
1084
1085 $all_tables = $wpdb->get_results("SHOW TABLES", ARRAY_N);
1086 $all_tables = array_map(create_function('$a', 'return $a[0];'), $all_tables);
1087
1088 $updraft_dir = $this->backups_dir_location();
1089 //get the blog name and rip out all non-alphanumeric chars other than _
1090 $blog_name = str_replace(' ','_',get_bloginfo());
1091 $blog_name = preg_replace('/[^A-Za-z0-9_]/','', $blog_name);
1092 if(!$blog_name) {
1093 $blog_name = 'non_alpha_name';
1094 }
1095
1096 $backup_file_base = $updraft_dir.'/backup_'.date('Y-m-d-Hi',$this->backup_time).'_'.$blog_name.'_'.$this->nonce;
1097 if (is_writable($updraft_dir)) {
1098 if (function_exists('gzopen')) {
1099 $this->dbhandle = @gzopen($backup_file_base.'-db.gz','w');
1100 } else {
1101 $this->dbhandle = @fopen($backup_file_base.'-db.gz', 'w');
1102 }
1103 if(!$this->dbhandle) {
1104 //$this->error(__('Could not open the backup file for writing!','wp-db-backup'));
1105 }
1106 } else {
1107 //$this->error(__('The backup directory is not writable!','wp-db-backup'));
1108 }
1109
1110 //Begin new backup of MySql
1111 $this->stow("# " . __('WordPress MySQL database backup','wp-db-backup') . "\n");
1112 $this->stow("#\n");
1113 $this->stow("# " . sprintf(__('Generated: %s','wp-db-backup'),date("l j. F Y H:i T")) . "\n");
1114 $this->stow("# " . sprintf(__('Hostname: %s','wp-db-backup'),DB_HOST) . "\n");
1115 $this->stow("# " . sprintf(__('Database: %s','wp-db-backup'),$this->backquote(DB_NAME)) . "\n");
1116 $this->stow("# --------------------------------------------------------\n");
1117
1118
1119 if (defined("DB_CHARSET")) {
1120 $this->stow("/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n");
1121 $this->stow("/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n");
1122 $this->stow("/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n");
1123 $this->stow("/*!40101 SET NAMES " . DB_CHARSET . " */;\n");
1124 }
1125 $this->stow("/*!40101 SET foreign_key_checks = 0 */;\n");
1126
1127 foreach ($all_tables as $table) {
1128 $total_tables++;
1129 // Increase script execution time-limit to 15 min for every table.
1130 if ( !ini_get('safe_mode') || strtolower(ini_get('safe_mode')) == "off") @set_time_limit(15*60);
1131 # === is needed, otherwise 'false' matches (i.e. prefix does not match)
1132 if ( strpos($table, $table_prefix) === 0 ) {
1133 // Create the SQL statements
1134 $this->stow("# --------------------------------------------------------\n");
1135 $this->stow("# " . sprintf(__('Table: %s','wp-db-backup'),$this->backquote($table)) . "\n");
1136 $this->stow("# --------------------------------------------------------\n");
1137 $this->backup_table($table);
1138 } else {
1139 $this->stow("# --------------------------------------------------------\n");
1140 $this->stow("# " . sprintf(__('Skipping non-WP table: %s','wp-db-backup'),$this->backquote($table)) . "\n");
1141 $this->stow("# --------------------------------------------------------\n");
1142 }
1143 }
1144
1145 if (defined("DB_CHARSET")) {
1146 $this->stow("/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;\n");
1147 $this->stow("/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;\n");
1148 $this->stow("/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;\n");
1149 }
1150
1151 $this->close($this->dbhandle);
1152
1153 if (count($this->errors)) {
1154 return false;
1155 } else {
1156 # Encrypt, if requested
1157 $encryption = get_option('updraft_encryptionphrase');
1158 if (strlen($encryption) > 0) {
1159 $this->log("Database: applying encryption");
1160 $encryption_error = 0;
1161 require_once(dirname(__FILE__).'/includes/Rijndael.php');
1162 $rijndael = new Crypt_Rijndael();
1163 $rijndael->setKey($encryption);
1164 $in_handle = @fopen($backup_file_base.'-db.gz','r');
1165 $buffer = "";
1166 while (!feof ($in_handle)) {
1167 $buffer .= fread($in_handle, 16384);
1168 }
1169 fclose ($in_handle);
1170 $out_handle = @fopen($backup_file_base.'-db.gz.crypt','w');
1171 if (!fwrite($out_handle, $rijndael->encrypt($buffer))) {$encryption_error = 1;}
1172 fclose ($out_handle);
1173 if (0 == $encryption_error) {
1174 # Delete unencrypted file
1175 @unlink($backup_file_base.'-db.gz');
1176 return basename($backup_file_base.'-db.gz.crypt');
1177 } else {
1178 $this->error("Encryption error occurred when encrypting database. Aborted.");
1179 }
1180 } else {
1181 return basename($backup_file_base.'-db.gz');
1182 }
1183 }
1184 $this->log("Total database tables backed up: $total_tables");
1185
1186 } //wp_db_backup
1187
1188 /**
1189 * Taken partially from phpMyAdmin and partially from
1190 * Alain Wolf, Zurich - Switzerland
1191 * Website: http://restkultur.ch/personal/wolf/scripts/db_backup/
1192 * Modified by Scott Merrill (http://www.skippy.net/)
1193 * to use the WordPress $wpdb object
1194 * @param string $table
1195 * @param string $segment
1196 * @return void
1197 */
1198 function backup_table($table, $segment = 'none') {
1199 global $wpdb;
1200
1201 $total_rows = 0;
1202
1203 $table_structure = $wpdb->get_results("DESCRIBE $table");
1204 if (! $table_structure) {
1205 //$this->error(__('Error getting table details','wp-db-backup') . ": $table");
1206 return false;
1207 }
1208
1209 if(($segment == 'none') || ($segment == 0)) {
1210 // Add SQL statement to drop existing table
1211 $this->stow("\n\n");
1212 $this->stow("#\n");
1213 $this->stow("# " . sprintf(__('Delete any existing table %s','wp-db-backup'),$this->backquote($table)) . "\n");
1214 $this->stow("#\n");
1215 $this->stow("\n");
1216 $this->stow("DROP TABLE IF EXISTS " . $this->backquote($table) . ";\n");
1217
1218 // Table structure
1219 // Comment in SQL-file
1220 $this->stow("\n\n");
1221 $this->stow("#\n");
1222 $this->stow("# " . sprintf(__('Table structure of table %s','wp-db-backup'),$this->backquote($table)) . "\n");
1223 $this->stow("#\n");
1224 $this->stow("\n");
1225
1226 $create_table = $wpdb->get_results("SHOW CREATE TABLE $table", ARRAY_N);
1227 if (false === $create_table) {
1228 $err_msg = sprintf(__('Error with SHOW CREATE TABLE for %s.','wp-db-backup'), $table);
1229 //$this->error($err_msg);
1230 $this->stow("#\n# $err_msg\n#\n");
1231 }
1232 $this->stow($create_table[0][1] . ' ;');
1233
1234 if (false === $table_structure) {
1235 $err_msg = sprintf(__('Error getting table structure of %s','wp-db-backup'), $table);
1236 //$this->error($err_msg);
1237 $this->stow("#\n# $err_msg\n#\n");
1238 }
1239
1240 // Comment in SQL-file
1241 $this->stow("\n\n");
1242 $this->stow("#\n");
1243 $this->stow('# ' . sprintf(__('Data contents of table %s','wp-db-backup'),$this->backquote($table)) . "\n");
1244 $this->stow("#\n");
1245 }
1246
1247 if(($segment == 'none') || ($segment >= 0)) {
1248 $defs = array();
1249 $ints = array();
1250 foreach ($table_structure as $struct) {
1251 if ( (0 === strpos($struct->Type, 'tinyint')) ||
1252 (0 === strpos(strtolower($struct->Type), 'smallint')) ||
1253 (0 === strpos(strtolower($struct->Type), 'mediumint')) ||
1254 (0 === strpos(strtolower($struct->Type), 'int')) ||
1255 (0 === strpos(strtolower($struct->Type), 'bigint')) ) {
1256 $defs[strtolower($struct->Field)] = ( null === $struct->Default ) ? 'NULL' : $struct->Default;
1257 $ints[strtolower($struct->Field)] = "1";
1258 }
1259 }
1260
1261
1262 // Batch by $row_inc
1263 if ( ! defined('ROWS_PER_SEGMENT') ) {
1264 define('ROWS_PER_SEGMENT', 100);
1265 }
1266
1267 if($segment == 'none') {
1268 $row_start = 0;
1269 $row_inc = ROWS_PER_SEGMENT;
1270 } else {
1271 $row_start = $segment * ROWS_PER_SEGMENT;
1272 $row_inc = ROWS_PER_SEGMENT;
1273 }
1274 do {
1275 // don't include extra stuff, if so requested
1276 $excs = array('revisions' => 0, 'spam' => 1); //TODO, FIX THIS
1277 $where = '';
1278 if ( is_array($excs['spam'] ) && in_array($table, $excs['spam']) ) {
1279 $where = ' WHERE comment_approved != "spam"';
1280 } elseif ( is_array($excs['revisions'] ) && in_array($table, $excs['revisions']) ) {
1281 $where = ' WHERE post_type != "revision"';
1282 }
1283
1284 if ( !ini_get('safe_mode') || strtolower(ini_get('safe_mode')) == "off") @set_time_limit(15*60);
1285 $table_data = $wpdb->get_results("SELECT * FROM $table $where LIMIT {$row_start}, {$row_inc}", ARRAY_A);
1286 $entries = 'INSERT INTO ' . $this->backquote($table) . ' VALUES (';
1287 // \x08\\x09, not required
1288 $search = array("\x00", "\x0a", "\x0d", "\x1a");
1289 $replace = array('\0', '\n', '\r', '\Z');
1290 if($table_data) {
1291 foreach ($table_data as $row) {
1292 $total_rows++;
1293 $values = array();
1294 foreach ($row as $key => $value) {
1295 if ($ints[strtolower($key)]) {
1296 // make sure there are no blank spots in the insert syntax,
1297 // yet try to avoid quotation marks around integers
1298 $value = ( null === $value || '' === $value) ? $defs[strtolower($key)] : $value;
1299 $values[] = ( '' === $value ) ? "''" : $value;
1300 } else {
1301 $values[] = "'" . str_replace($search, $replace, $this->sql_addslashes($value)) . "'";
1302 }
1303 }
1304 $this->stow(" \n" . $entries . implode(', ', $values) . ');');
1305 }
1306 $row_start += $row_inc;
1307 }
1308 } while((count($table_data) > 0) and ($segment=='none'));
1309 }
1310
1311 if(($segment == 'none') || ($segment < 0)) {
1312 // Create footer/closing comment in SQL-file
1313 $this->stow("\n");
1314 $this->stow("#\n");
1315 $this->stow("# " . sprintf(__('End of data contents of table %s','wp-db-backup'),$this->backquote($table)) . "\n");
1316 $this->stow("# --------------------------------------------------------\n");
1317 $this->stow("\n");
1318 }
1319 $this->log("Table $table: Total rows added: $total_rows");
1320
1321 } // end backup_table()
1322
1323
1324 function stow($query_line) {
1325 if (function_exists('gzopen')) {
1326 if(! @gzwrite($this->dbhandle, $query_line)) {
1327 //$this->error(__('There was an error writing a line to the backup script:','wp-db-backup') . ' ' . $query_line . ' ' . $php_errormsg);
1328 }
1329 } else {
1330 if(false === @fwrite($this->dbhandle, $query_line)) {
1331 //$this->error(__('There was an error writing a line to the backup script:','wp-db-backup') . ' ' . $query_line . ' ' . $php_errormsg);
1332 }
1333 }
1334 }
1335
1336
1337 function close($handle) {
1338 if (function_exists('gzopen')) {
1339 gzclose($handle);
1340 } else {
1341 fclose($handle);
1342 }
1343 }
1344
1345 /**
1346 * Logs any error messages
1347 * @param array $args
1348 * @return bool
1349 */
1350 function error($error,$severity='') {
1351 $this->errors[] = array('error'=>$error,'severity'=>$severity);
1352 if ($severity == 'fatal') {
1353 //do something...
1354 }
1355 return true;
1356 }
1357
1358 /**
1359 * Add backquotes to tables and db-names in
1360 * SQL queries. Taken from phpMyAdmin.
1361 */
1362 function backquote($a_name) {
1363 if (!empty($a_name) && $a_name != '*') {
1364 if (is_array($a_name)) {
1365 $result = array();
1366 reset($a_name);
1367 while(list($key, $val) = each($a_name))
1368 $result[$key] = '`' . $val . '`';
1369 return $result;
1370 } else {
1371 return '`' . $a_name . '`';
1372 }
1373 } else {
1374 return $a_name;
1375 }
1376 }
1377
1378 /**
1379 * Better addslashes for SQL queries.
1380 * Taken from phpMyAdmin.
1381 */
1382 function sql_addslashes($a_string = '', $is_like = false) {
1383 if ($is_like) $a_string = str_replace('\\', '\\\\\\\\', $a_string);
1384 else $a_string = str_replace('\\', '\\\\', $a_string);
1385 return str_replace('\'', '\\\'', $a_string);
1386 }
1387
1388 /*END OF WP-DB-BACKUP BLOCK */
1389
1390 /*
1391 this function is both the backup scheduler and ostensibly a filter callback for saving the option.
1392 it is called in the register_setting for the updraft_interval, which means when the admin settings
1393 are saved it is called. it returns the actual result from wp_filter_nohtml_kses (a sanitization filter)
1394 so the option can be properly saved.
1395 */
1396 function schedule_backup($interval) {
1397 //clear schedule and add new so we don't stack up scheduled backups
1398 wp_clear_scheduled_hook('updraft_backup');
1399 switch($interval) {
1400 case 'daily':
1401 case 'weekly':
1402 case 'monthly':
1403 wp_schedule_event(time()+30, $interval, 'updraft_backup');
1404 break;
1405 }
1406 return wp_filter_nohtml_kses($interval);
1407 }
1408
1409 function schedule_backup_database($interval) {
1410 //clear schedule and add new so we don't stack up scheduled backups
1411 wp_clear_scheduled_hook('updraft_backup_database');
1412 switch($interval) {
1413 case 'daily':
1414 case 'weekly':
1415 case 'monthly':
1416 wp_schedule_event(time()+30, $interval, 'updraft_backup_database');
1417 break;
1418 }
1419 return wp_filter_nohtml_kses($interval);
1420 }
1421
1422 //wp-cron only has hourly, daily and twicedaily, so we need to add weekly and monthly.
1423 function modify_cron_schedules($schedules) {
1424 $schedules['weekly'] = array(
1425 'interval' => 604800,
1426 'display' => 'Once Weekly'
1427 );
1428 $schedules['monthly'] = array(
1429 'interval' => 2592000,
1430 'display' => 'Once Monthly'
1431 );
1432 return $schedules;
1433 }
1434
1435 function backups_dir_location() {
1436 $updraft_dir = untrailingslashit(get_option('updraft_dir'));
1437 $default_backup_dir = WP_CONTENT_DIR.'/updraft';
1438 //if the option isn't set, default it to /backups inside the upload dir
1439 $updraft_dir = ($updraft_dir)?$updraft_dir:$default_backup_dir;
1440 //check for the existence of the dir and an enumeration preventer.
1441 if(!is_dir($updraft_dir) || !is_file($updraft_dir.'/index.html') || !is_file($updraft_dir.'/.htaccess')) {
1442 @mkdir($updraft_dir,0777,true); //recursively create the dir with 0777 permissions. 0777 is default for php creation. not ideal, but I'll get back to this
1443 @file_put_contents($updraft_dir.'/index.html','Nothing to see here.');
1444 @file_put_contents($updraft_dir.'/.htaccess','deny from all');
1445 }
1446 return $updraft_dir;
1447 }
1448
1449 function updraft_download_backup() {
1450 $type = $_POST['type'];
1451 $timestamp = (int)$_POST['timestamp'];
1452 $backup_history = $this->get_backup_history();
1453 $file = $backup_history[$timestamp][$type];
1454 $fullpath = trailingslashit(get_option('updraft_dir')).$file;
1455 if(!is_readable($fullpath)) {
1456 //if the file doesn't exist and they're using one of the cloud options, fetch it down from the cloud.
1457 $this->download_backup($file);
1458 }
1459 if(@is_readable($fullpath) && is_file($fullpath)) {
1460 $len = filesize($fullpath);
1461
1462 $filearr = explode('.',$file);
1463 //we've only got zip and gz...for now
1464 $file_ext = array_pop($filearr);
1465 if($file_ext == 'zip') {
1466 header('Content-type: application/zip');
1467 } else {
1468 // This catches both when what was popped was 'crypt' (*-db.gz.crypt) and when it was 'gz' (unencrypted)
1469 header('Content-type: application/x-gzip');
1470 }
1471 header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1
1472 header("Expires: Sat, 26 Jul 1997 05:00:00 GMT"); // Date in the past
1473 header("Content-Length: $len;");
1474 if ($file_ext == 'crypt') {
1475 header("Content-Disposition: attachment; filename=\"".substr($file,0,-6)."\";");
1476 } else {
1477 header("Content-Disposition: attachment; filename=\"$file\";");
1478 }
1479 ob_end_flush();
1480 if ($file_ext == 'crypt') {
1481 $encryption = get_option('updraft_encryptionphrase');
1482 if ($encryption == "") {
1483 $this->error('Decryption of database failed: the database file is encrypted, but you have no encryption key entered.');
1484 } else {
1485 require_once(dirname(__FILE__).'/includes/Rijndael.php');
1486 $rijndael = new Crypt_Rijndael();
1487 $rijndael->setKey($encryption);
1488 $in_handle = fopen($fullpath,'r');
1489 $ciphertext = "";
1490 while (!feof ($in_handle)) {
1491 $ciphertext .= fread($in_handle, 16384);
1492 }
1493 fclose ($in_handle);
1494 print $rijndael->decrypt($ciphertext);
1495 }
1496 } else {
1497 readfile($fullpath);
1498 }
1499 $this->delete_local($file);
1500 exit; //we exit immediately because otherwise admin-ajax appends an additional zero to the end for some reason I don't understand. seriously, why die('0')?
1501 } else {
1502 echo 'Download failed. File '.$fullpath.' did not exist or was unreadable. If you delete local backups then S3 or Google Drive or FTP retrieval may have failed. (Note that Google Drive downloading is not yet supported - you need to download manually if you use Google Drive).';
1503 }
1504 }
1505
1506 function download_backup($file) {
1507 switch(get_option('updraft_service')) {
1508 case 'googledrive':
1509 $this->download_googledrive_backup($file);
1510 break;
1511 case 's3':
1512 $this->download_s3_backup($file);
1513 break;
1514 case 'ftp':
1515 $this->download_ftp_backup($file);
1516 break;
1517 default:
1518 $this->error('Automatic backup restoration is only available via S3, FTP, and local. Email and downloaded backup restoration must be performed manually.');
1519 }
1520 }
1521
1522 function download_googledrive_backup($file) {
1523 $this->error("Google Drive error: we do not yet support downloading existing backups from Google Drive - you need to restore the backup manually");
1524 }
1525
1526 function download_s3_backup($file) {
1527 if(!class_exists('S3')) {
1528 require_once(dirname(__FILE__).'/includes/S3.php');
1529 }
1530 $s3 = new S3(get_option('updraft_s3_login'), get_option('updraft_s3_pass'));
1531 $bucket_name = untrailingslashit(get_option('updraft_s3_remote_path'));
1532 $bucket_path = "";
1533 if (preg_match("#^([^/]+)/(.*)$#",$bucket_name,$bmatches)) {
1534 $bucket_name = $bmatches[1];
1535 $bucket_path = $bmatches[2]."/";
1536 }
1537 if (@$s3->putBucket($bucket_name, S3::ACL_PRIVATE)) {
1538 $fullpath = trailingslashit(get_option('updraft_dir')).$file;
1539 if (!$s3->getObject($bucket_name, $bucket_path.$file, $fullpath)) {
1540 $this->error("S3 Error: Failed to download $fullpath. Error was ".$php_errormsg);
1541 }
1542 } else {
1543 $this->error("S3 Error: Failed to create bucket $bucket_name. Error was ".$php_errormsg);
1544 }
1545 }
1546
1547 function download_ftp_backup($file) {
1548 if( !class_exists('ftp_wrapper')) {
1549 require_once(dirname(__FILE__).'/includes/ftp.class.php');
1550 }
1551 //handle SSL and errors at some point TODO
1552 $ftp = new ftp_wrapper(get_option('updraft_server_address'),get_option('updraft_ftp_login'),get_option('updraft_ftp_pass'));
1553 $ftp->passive = true;
1554 $ftp->connect();
1555 //$ftp->make_dir(); we may need to recursively create dirs? TODO
1556
1557 $ftp_remote_path = trailingslashit(get_option('updraft_ftp_remote_path'));
1558 $fullpath = trailingslashit(get_option('updraft_dir')).$file;
1559 $ftp->get($fullpath,$ftp_remote_path.$file,FTP_BINARY);
1560 }
1561
1562 function restore_backup($timestamp) {
1563 global $wp_filesystem;
1564 $backup_history = get_option('updraft_backup_history');
1565 if(!is_array($backup_history[$timestamp])) {
1566 echo '<p>This backup does not exist in the backup history - restoration aborted. Timestamp: '.$timestamp.'</p><br/>';
1567 return false;
1568 }
1569
1570 $credentials = request_filesystem_credentials("options-general.php?page=updraftplus&action=updraft_restore&backup_timestamp=$timestamp");
1571 WP_Filesystem($credentials);
1572 if ( $wp_filesystem->errors->get_error_code() ) {
1573 foreach ( $wp_filesystem->errors->get_error_messages() as $message )
1574 show_message($message);
1575 exit;
1576 }
1577
1578 //if we make it this far then WP_Filesystem has been instantiated and is functional (tested with ftpext, what about suPHP and other situations where direct may work?)
1579 echo '<span style="font-weight:bold">Restoration Progress </span><div id="updraft-restore-progress">';
1580
1581 $updraft_dir = trailingslashit(get_option('updraft_dir'));
1582 foreach($backup_history[$timestamp] as $type=>$file) {
1583 $fullpath = $updraft_dir.$file;
1584 if(!is_readable($fullpath) && $type != 'db') {
1585 $this->download_backup($file);
1586 }
1587 # Types: uploads, themes, plugins, others, db
1588 if(is_readable($fullpath) && $type != 'db') {
1589 if(!class_exists('WP_Upgrader')) {
1590 require_once( ABSPATH . 'wp-admin/includes/class-wp-upgrader.php' );
1591 }
1592 require_once('includes/updraft-restorer.php');
1593 $restorer = new Updraft_Restorer();
1594 $val = $restorer->restore_backup($fullpath,$type);
1595 if(is_wp_error($val)) {
1596 print_r($val);
1597 echo '</div>'; //close the updraft_restore_progress div even if we error
1598 return false;
1599 }
1600 }
1601 }
1602 echo '</div>'; //close the updraft_restore_progress div
1603 # The 'off' check is for badly configured setups - http://wordpress.org/support/topic/plugin-wp-super-cache-warning-php-safe-mode-enabled-but-safe-mode-is-off
1604 if(ini_get('safe_mode') && strtolower(ini_get('safe_mode')) != "off") {
1605 echo "<p>DB could not be restored because safe_mode is active on your server. You will need to manually restore the file via phpMyAdmin or another method.</p><br/>";
1606 return false;
1607 }
1608 return true;
1609 }
1610
1611 //deletes the -old directories that are created when a backup is restored.
1612 function delete_old_dirs() {
1613 global $wp_filesystem;
1614 $credentials = request_filesystem_credentials("options-general.php?page=updraftplus&action=updraft_delete_old_dirs");
1615 WP_Filesystem($credentials);
1616 if ( $wp_filesystem->errors->get_error_code() ) {
1617 foreach ( $wp_filesystem->errors->get_error_messages() as $message )
1618 show_message($message);
1619 exit;
1620 }
1621
1622 $to_delete = array('themes-old','plugins-old','uploads-old','others-old');
1623
1624 foreach($to_delete as $name) {
1625 //recursively delete
1626 if(!$wp_filesystem->delete(WP_CONTENT_DIR.'/'.$name, true)) {
1627 return false;
1628 }
1629 }
1630 return true;
1631 }
1632
1633 //scans the content dir to see if any -old dirs are present
1634 function scan_old_dirs() {
1635 $dirArr = scandir(WP_CONTENT_DIR);
1636 foreach($dirArr as $dir) {
1637 if(strpos($dir,'-old') !== false) {
1638 return true;
1639 }
1640 }
1641 return false;
1642 }
1643
1644
1645 function retain_range($input) {
1646 $input = (int)$input;
1647 if($input > 0 && $input < 3650) {
1648 return $input;
1649 } else {
1650 return 1;
1651 }
1652 }
1653
1654 function create_backup_dir() {
1655 global $wp_filesystem;
1656 $credentials = request_filesystem_credentials("options-general.php?page=updraftplus&action=updraft_create_backup_dir");
1657 WP_Filesystem($credentials);
1658 if ( $wp_filesystem->errors->get_error_code() ) {
1659 foreach ( $wp_filesystem->errors->get_error_messages() as $message )
1660 show_message($message);
1661 exit;
1662 }
1663
1664 $updraft_dir = untrailingslashit(get_option('updraft_dir'));
1665 $default_backup_dir = WP_CONTENT_DIR.'/updraft';
1666 $updraft_dir = ($updraft_dir)?$updraft_dir:$default_backup_dir;
1667
1668 //chmod the backup dir to 0777. ideally we'd rather chgrp it but i'm not sure if it's possible to detect the group apache is running under (or what if it's not apache...)
1669 if(!$wp_filesystem->mkdir($updraft_dir, 0777)) {
1670 return false;
1671 }
1672 return true;
1673 }
1674
1675
1676 function memory_check_current() {
1677 # Returns in megabytes
1678 $memory_limit = ini_get('memory_limit');
1679 $memory_unit = $memory_limit[strlen($memory_limit)-1];
1680 $memory_limit = substr($memory_limit,0,strlen($memory_limit)-1);
1681 switch($memory_unit) {
1682 case 'K':
1683 $memory_limit = $memory_limit/1024;
1684 break;
1685 case 'G':
1686 $memory_limit = $memory_limit*1024;
1687 break;
1688 case 'M':
1689 //assumed size, no change needed
1690 break;
1691 }
1692 return $memory_limit;
1693 }
1694
1695 function memory_check($memory) {
1696 $memory_limit = $this->memory_check_current();
1697 return ($memory_limit >= $memory)?true:false;
1698 }
1699
1700 function execution_time_check($time) {
1701 return (ini_get('max_execution_time') >= $time)?true:false;
1702 }
1703
1704 function admin_init() {
1705 if(get_option('updraft_debug_mode')) {
1706 ini_set('display_errors',1);
1707 error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED);
1708 ini_set('track_errors',1);
1709 }
1710 wp_enqueue_script('jquery');
1711 register_setting( 'updraft-options-group', 'updraft_interval', array($this,'schedule_backup') );
1712 register_setting( 'updraft-options-group', 'updraft_interval_database', array($this,'schedule_backup_database') );
1713 register_setting( 'updraft-options-group', 'updraft_retain', array($this,'retain_range') );
1714 register_setting( 'updraft-options-group', 'updraft_encryptionphrase', 'wp_filter_nohtml_kses' );
1715 register_setting( 'updraft-options-group', 'updraft_service', 'wp_filter_nohtml_kses' );
1716 register_setting( 'updraft-options-group', 'updraft_s3_login', 'wp_filter_nohtml_kses' );
1717 register_setting( 'updraft-options-group', 'updraft_s3_pass', 'wp_filter_nohtml_kses' );
1718 register_setting( 'updraft-options-group', 'updraft_s3_remote_path', 'wp_filter_nohtml_kses' );
1719 register_setting( 'updraft-options-group', 'updraft_googledrive_clientid', 'wp_filter_nohtml_kses' );
1720 register_setting( 'updraft-options-group', 'updraft_googledrive_secret', 'wp_filter_nohtml_kses' );
1721 register_setting( 'updraft-options-group', 'updraft_googledrive_remotepath', 'wp_filter_nohtml_kses' );
1722 register_setting( 'updraft-options-group', 'updraft_ftp_login', 'wp_filter_nohtml_kses' );
1723 register_setting( 'updraft-options-group', 'updraft_ftp_pass', 'wp_filter_nohtml_kses' );
1724 register_setting( 'updraft-options-group', 'updraft_dir', 'wp_filter_nohtml_kses' );
1725 register_setting( 'updraft-options-group', 'updraft_email', 'wp_filter_nohtml_kses' );
1726 register_setting( 'updraft-options-group', 'updraft_ftp_remote_path', 'wp_filter_nohtml_kses' );
1727 register_setting( 'updraft-options-group', 'updraft_server_address', 'wp_filter_nohtml_kses' );
1728 register_setting( 'updraft-options-group', 'updraft_delete_local', 'absint' );
1729 register_setting( 'updraft-options-group', 'updraft_debug_mode', 'absint' );
1730 register_setting( 'updraft-options-group', 'updraft_include_plugins', 'absint' );
1731 register_setting( 'updraft-options-group', 'updraft_include_themes', 'absint' );
1732 register_setting( 'updraft-options-group', 'updraft_include_uploads', 'absint' );
1733 register_setting( 'updraft-options-group', 'updraft_include_others', 'absint' );
1734 register_setting( 'updraft-options-group', 'updraft_include_others_exclude', 'wp_filter_nohtml_kses' );
1735
1736 /* I see no need for this check; people can only download backups/logs if they can guess a nonce formed from a random number and if .htaccess files have no effect. The database will be encrypted. Very unlikely.
1737 if (current_user_can('manage_options')) {
1738 $updraft_dir = $this->backups_dir_location();
1739 if(strpos($updraft_dir,WP_CONTENT_DIR) !== false) {
1740 $relative_dir = str_replace(WP_CONTENT_DIR,'',$updraft_dir);
1741 $possible_updraft_url = WP_CONTENT_URL.$relative_dir;
1742 $resp = wp_remote_request($possible_updraft_url, array('timeout' => 15));
1743 if ( is_wp_error($resp) ) {
1744 add_action('admin_notices', array($this,'show_admin_warning_accessible_unknownresult') );
1745 } else {
1746 if(strpos($resp['response']['code'],'403') === false) {
1747 add_action('admin_notices', array($this,'show_admin_warning_accessible') );
1748 }
1749 }
1750 }
1751 }
1752 */
1753 if (current_user_can('manage_options') && get_option('updraft_service') == "googledrive" && get_option('updraft_googledrive_clientid') != "" && get_option('updraft_googledrive_token','xyz') == 'xyz') {
1754 add_action('admin_notices', array($this,'show_admin_warning_googledrive') );
1755 }
1756 }
1757
1758 function add_admin_pages() {
1759 add_submenu_page('options-general.php', "UpdraftPlus", "UpdraftPlus", "manage_options", "updraftplus",
1760 array($this,"settings_output"));
1761 }
1762
1763 function wordshell_random_advert($urls) {
1764 $url_start = ($urls) ? '<a href="http://wordshell.net">' : "";
1765 $url_end = ($urls) ? '</a>' : " (www.wordshell.net)";
1766 if (rand(0,1) == 0) {
1767 return "Like automating WordPress operations? Use the CLI? ${url_start}You will love WordShell${url_end} - saves time and money fast.";
1768 } else {
1769 return "${url_start}Check out WordShell${url_end} - manage WordPress from the command line - huge time-saver";
1770 }
1771 }
1772
1773 function settings_output() {
1774
1775 $ws_advert = $this->wordshell_random_advert(1);
1776 echo <<<ENDHERE
1777 <div class="updated fade" style="font-size:140%; padding:14px;">${ws_advert}</div>
1778 ENDHERE;
1779
1780 /*
1781 we use request here because the initial restore is triggered by a POSTed form. we then may need to obtain credentials
1782 for the WP_Filesystem. to do this WP outputs a form that we can't insert variables into (apparently). So the values are
1783 passed back in as GET parameters. REQUEST covers both GET and POST so this weird logic works.
1784 */
1785 if(isset($_REQUEST['action']) && $_REQUEST['action'] == 'updraft_restore' && isset($_REQUEST['backup_timestamp'])) {
1786 $backup_success = $this->restore_backup($_REQUEST['backup_timestamp']);
1787 if(empty($this->errors) && $backup_success == true) {
1788 echo '<p>Restore successful!</p><br/>';
1789 echo '<b>Actions:</b> <a href="options-general.php?page=updraftplus&updraft_restore_success=true">Return to Updraft Configuration</a>.';
1790 return;
1791 } else {
1792 echo '<p>Restore failed...</p><br/>';
1793 echo '<b>Actions:</b> <a href="options-general.php?page=updraftplus">Return to Updraft Configuration</a>.';
1794 return;
1795 }
1796 //uncomment the below once i figure out how i want the flow of a restoration to work.
1797 //echo '<b>Actions:</b> <a href="options-general.php?page=updraftplus">Return to Updraft Configuration</a>.';
1798 }
1799 $deleted_old_dirs = false;
1800 if(isset($_REQUEST['action']) && $_REQUEST['action'] == 'updraft_delete_old_dirs') {
1801 if($this->delete_old_dirs()) {
1802 $deleted_old_dirs = true;
1803 } else {
1804 echo '<p>Old directory removal failed for some reason. You may want to do this manually.</p><br/>';
1805 }
1806 echo '<p>Old directories successfully removed.</p><br/>';
1807 echo '<b>Actions:</b> <a href="options-general.php?page=updraftplus">Return to Updraft Configuration</a>.';
1808 return;
1809 }
1810
1811 if(isset($_GET['error'])) {
1812 echo "<p><strong>ERROR:</strong> ".htmlspecialchars($_GET['error'])."</p>";
1813 }
1814 if(isset($_GET['message'])) {
1815 echo "<p><strong>Note:</strong> ".htmlspecialchars($_GET['message'])."</p>";
1816 }
1817
1818 if(isset($_GET['action']) && $_GET['action'] == 'updraft_create_backup_dir') {
1819 if(!$this->create_backup_dir()) {
1820 echo '<p>Backup directory could not be created...</p><br/>';
1821 }
1822 echo '<p>Backup directory successfully created.</p><br/>';
1823 echo '<b>Actions:</b> <a href="options-general.php?page=updraftplus">Return to Updraft Configuration</a>.';
1824 return;
1825 }
1826
1827 if(isset($_POST['action']) && $_POST['action'] == 'updraft_backup') {
1828 if (wp_schedule_single_event(time()+5, 'updraft_backup_all') === false) {
1829 echo "<!-- updraft schedule: failed -->";
1830 } else {
1831 echo "<!-- updraft schedule: ok -->";
1832 }
1833 }
1834 if(isset($_POST['action']) && $_POST['action'] == 'updraft_backup_debug_all') {
1835 $this->backup(true,true);
1836 }
1837 if(isset($_POST['action']) && $_POST['action'] == 'updraft_backup_debug_db') {
1838 $this->backup_db();
1839 }
1840
1841 ?>
1842 <div class="wrap">
1843 <h2>UpdraftPlus - Backup/Restore</h2>
1844
1845 Version: <b><?php echo $this->version; ?></b><br />
1846 Maintained by <b>David Anderson</b> (<a href="http://david.dw-perspective.org.uk">Homepage</a> | <a href="http://wordshell.net">WordShell - WordPress command line</a> | <a href="http://david.dw-perspective.org.uk/donate">Donate</a> | <a href="http://wordpress.org/extend/plugins/updraftplus/faq/">FAQs</a>)
1847 <br />
1848 Based on Updraft by <b>Paul Kehrer</b> (<a href="http://langui.sh" target="_blank">Blog</a> | <a href="http://twitter.com/reaperhulk" target="_blank">Twitter</a> )
1849 <br />
1850 <?php
1851 if(isset($_GET['updraft_restore_success'])) {
1852 echo "<div style=\"color:blue\">Your backup has been restored. Your old themes, uploads, and plugins directories have been retained with \"-old\" appended to their name. Remove them when you are satisfied that the backup worked properly. At this time Updraft does not automatically restore your DB. You will need to use an external tool like phpMyAdmin to perform that task.</div>";
1853 }
1854 if($deleted_old_dirs) {
1855 echo "<div style=\"color:blue\">Old directories successfully deleted.</div>";
1856 }
1857 if(!$this->memory_check(96)) {?>
1858 <div style="color:orange">Your PHP memory limit is too low. Updraft attempted to raise it but was unsuccessful. This plugin may not work properly with a memory limit of less than 96 Mb (though on the other hand, it has been used successfully with a 32Mb limit - your mileage may vary, but don't blame us!). Current limit is: <?php echo $this->memory_check_current(); ?> Mb</div>
1859 <?php
1860 }
1861 if(!$this->execution_time_check(300)) {?>
1862 <div style="color:orange">Your PHP max_execution_time is less than 300 seconds. This probably means you're running in safe_mode. Either disable safe_mode or modify your php.ini to set max_execution_time to a higher number. If you do not, there is a chance Updraft will be unable to complete a backup. Present limit is: <?php echo ini_get('max_execution_time'); ?> seconds.</div>
1863 <?php
1864 }
1865
1866 if($this->scan_old_dirs()) {?>
1867 <div style="color:orange">You have old directories from a previous backup. Click to delete them after you have verified that the restoration worked.</div>
1868 <form method="post" action="<?php echo remove_query_arg(array('updraft_restore_success','action')) ?>">
1869 <input type="hidden" name="action" value="updraft_delete_old_dirs" />
1870 <input type="submit" class="button-primary" value="Delete Old Dirs" onclick="return(confirm('Are you sure you want to delete the old directories? This cannot be undone.'))" />
1871 </form>
1872 <?php
1873 }
1874 if(!empty($this->errors)) {
1875 foreach($this->errors as $error) {
1876 //ignoring severity here right now
1877 echo '<div style="color:red">'.$error['error'].'</div>';
1878 }
1879 }
1880 ?>
1881 <table class="form-table" style="float:left;width:475px">
1882 <tr>
1883 <?php
1884 $next_scheduled_backup = wp_next_scheduled('updraft_backup');
1885 $next_scheduled_backup = ($next_scheduled_backup) ? date('D, F j, Y H:i T',$next_scheduled_backup) : 'No backups are scheduled at this time.';
1886 $next_scheduled_backup_database = wp_next_scheduled('updraft_backup_database');
1887 if (get_option('updraft_interval_database',get_option('updraft_interval')) == get_option('updraft_interval')) {
1888 $next_scheduled_backup_database = "Will take place at the same time as the files backup.";
1889 } else {
1890 $next_scheduled_backup_database = ($next_scheduled_backup_database) ? date('D, F j, Y H:i T',$next_scheduled_backup_database) : 'No backups are scheduled at this time.';
1891 }
1892 $current_time = date('D, F j, Y H:i T',time());
1893 $updraft_last_backup = get_option('updraft_last_backup');
1894 if($updraft_last_backup) {
1895 if($updraft_last_backup['success']) {
1896 $last_backup = date('D, F j, Y H:i T',$updraft_last_backup['backup_time']);
1897 $last_backup_color = 'green';
1898 } else {
1899 $last_backup = print_r($updraft_last_backup['errors'],true);
1900 $last_backup_color = 'red';
1901 }
1902 } else {
1903 $last_backup = 'No backup has been completed.';
1904 $last_backup_color = 'blue';
1905 }
1906
1907 $updraft_dir = $this->backups_dir_location();
1908 if(is_writable($updraft_dir)) {
1909 $dir_info = '<span style="color:green">Backup directory specified is writable, which is good.</span>';
1910 $backup_disabled = "";
1911 } else {
1912 $backup_disabled = 'disabled="disabled"';
1913 $dir_info = '<span style="color:red">Backup directory specified is <b>not</b> writable, or does not exist. <span style="font-size:110%;font-weight:bold"><a href="options-general.php?page=updraftplus&action=updraft_create_backup_dir">Click here</a></span> to attempt to create the directory and set the permissions. If that is unsuccessful check the permissions on your server or change it to another directory that is writable by your web server process.</span>';
1914 }
1915 ?>
1916 <th>Now:</th>
1917 <td style="color:blue"><?php echo $current_time?></td>
1918 </tr>
1919 <tr>
1920 <th>Next Scheduled Files Backup:</th>
1921 <td style="color:blue"><?php echo $next_scheduled_backup?></td>
1922 </tr>
1923 <tr>
1924 <th>Next Scheduled DB Backup:</th>
1925 <td style="color:blue"><?php echo $next_scheduled_backup_database?></td>
1926 </tr>
1927 <tr>
1928 <th>Last Backup:</th>
1929 <td style="color:<?php echo $last_backup_color ?>"><?php echo $last_backup?></td>
1930 </tr>
1931 </table>
1932 <div style="float:left;width:200px">
1933 <form method="post" action="">
1934 <input type="hidden" name="action" value="updraft_backup" />
1935 <p><input type="submit" <?php echo $backup_disabled ?> class="button-primary" value="Backup Now!" style="padding-top:7px;padding-bottom:7px;font-size:24px !important" onclick="return(confirm('This will schedule a one time backup. To trigger the backup immediately you may need to load a page on your site.'))" /></p>
1936 </form>
1937 <div style="position:relative">
1938 <div style="position:absolute;top:0;left:0">
1939 <?php
1940 $backup_history = get_option('updraft_backup_history');
1941 $backup_history = (is_array($backup_history))?$backup_history:array();
1942 $restore_disabled = (count($backup_history) == 0) ? 'disabled="disabled"' : "";
1943 ?>
1944 <input type="button" class="button-primary" <?php echo $restore_disabled ?> value="Restore" style="padding-top:7px;padding-bottom:7px;font-size:24px !important" onclick="jQuery('#backup-restore').fadeIn('slow');jQuery(this).parent().fadeOut('slow')" />
1945 </div>
1946 <div style="display:none;position:absolute;top:0;left:0" id="backup-restore">
1947 <form method="post" action="">
1948 <b>Choose: </b>
1949 <select name="backup_timestamp" style="display:inline">
1950 <?php
1951 foreach($backup_history as $key=>$value) {
1952 echo "<option value='$key'>".date('Y-m-d G:i',$key)."</option>\n";
1953 }
1954 ?>
1955 </select>
1956
1957 <input type="hidden" name="action" value="updraft_restore" />
1958 <input type="submit" <?php echo $restore_disabled ?> class="button-primary" value="Restore Now!" style="padding-top:7px;margin-top:5px;padding-bottom:7px;font-size:24px !important" onclick="return(confirm('Restoring from backup will replace this site\'s themes, plugins, uploads and other content directories (according to what is contained in the backup set which you select). Database restoration cannot be done through this process - you must download the database and import yourself (e.g. through PHPMyAdmin). Do you wish to continue with the restoration process?'))" />
1959 </form>
1960 </div>
1961 </div>
1962 </div>
1963 <br style="clear:both" />
1964 <table class="form-table">
1965 <tr>
1966 <th>Download Backups</th>
1967 <td><a href="#" title="Click to see available backups" onclick="jQuery('.download-backups').toggle();return false;"><?php echo count($backup_history)?> available</a></td>
1968 </tr>
1969 <tr>
1970 <td></td><td class="download-backups" style="display:none">
1971 <em>Click on a button to download the corresponding file to your computer. If you are using Opera, you should turn Turbo mode off.</em>
1972 <table>
1973 <?php
1974 foreach($backup_history as $key=>$value) {
1975 ?>
1976 <tr>
1977 <td><b><?php echo date('Y-m-d G:i',$key)?></b></td>
1978 <td>
1979 <?php if (isset($value['db'])) { ?>
1980 <form action="admin-ajax.php" method="post">
1981 <input type="hidden" name="action" value="updraft_download_backup" />
1982 <input type="hidden" name="type" value="db" />
1983 <input type="hidden" name="timestamp" value="<?php echo $key?>" />
1984 <input type="submit" value="Database" />
1985 </form>
1986 <?php } else { echo "(No database in backup)"; } ?>
1987 </td>
1988 <td>
1989 <?php if (isset($value['plugins'])) { ?>
1990 <form action="admin-ajax.php" method="post">
1991 <input type="hidden" name="action" value="updraft_download_backup" />
1992 <input type="hidden" name="type" value="plugins" />
1993 <input type="hidden" name="timestamp" value="<?php echo $key?>" />
1994 <input type="submit" value="Plugins" />
1995 </form>
1996 <?php } else { echo "(No plugins in backup)"; } ?>
1997 </td>
1998 <td>
1999 <?php if (isset($value['themes'])) { ?>
2000 <form action="admin-ajax.php" method="post">
2001 <input type="hidden" name="action" value="updraft_download_backup" />
2002 <input type="hidden" name="type" value="themes" />
2003 <input type="hidden" name="timestamp" value="<?php echo $key?>" />
2004 <input type="submit" value="Themes" />
2005 </form>
2006 <?php } else { echo "(No themes in backup)"; } ?>
2007 </td>
2008 <td>
2009 <?php if (isset($value['uploads'])) { ?>
2010 <form action="admin-ajax.php" method="post">
2011 <input type="hidden" name="action" value="updraft_download_backup" />
2012 <input type="hidden" name="type" value="uploads" />
2013 <input type="hidden" name="timestamp" value="<?php echo $key?>" />
2014 <input type="submit" value="Uploads" />
2015 </form>
2016 <?php } else { echo "(No uploads in backup)"; } ?>
2017 </td>
2018 <td>
2019 <?php if (isset($value['others'])) { ?>
2020 <form action="admin-ajax.php" method="post">
2021 <input type="hidden" name="action" value="updraft_download_backup" />
2022 <input type="hidden" name="type" value="others" />
2023 <input type="hidden" name="timestamp" value="<?php echo $key?>" />
2024 <input type="submit" value="Others" />
2025 </form>
2026 <?php } else { echo "(No others in backup)"; } ?>
2027 </td>
2028 </tr>
2029 <?php }?>
2030 </table>
2031 </td>
2032 </tr>
2033 </table>
2034 <form method="post" action="options.php">
2035 <?php settings_fields('updraft-options-group'); ?>
2036 <table class="form-table">
2037 <tr>
2038 <th>Backup Directory:</th>
2039 <td><input type="text" name="updraft_dir" style="width:525px" value="<?php echo $updraft_dir ?>" /></td>
2040 </tr>
2041 <tr>
2042 <td></td><td><?php echo $dir_info ?> This is where Updraft Backup/Restore will write the zip files it creates initially. This directory must be writable by your web server. Typically you'll want to have it inside your wp-content folder (this is the default). <b>Do not</b> place it inside your uploads dir, as that will cause recursion issues (backups of backups of backups of...).</td>
2043 </tr>
2044 <tr>
2045 <th>File Backup Intervals:</th>
2046 <td><select name="updraft_interval">
2047 <?php
2048 $intervals = array ("manual", "daily", "weekly", "monthly");
2049 foreach ($intervals as $ival) {
2050 echo "<option value=\"$ival\" ";
2051 if ($ival == get_option('updraft_interval','manual')) { echo 'selected="selected"';}
2052 echo ">".ucfirst($ival)."</option>\n";
2053 }
2054 ?>
2055 </select></td>
2056 </tr>
2057 <tr>
2058 <th>Database Backup Intervals:</th>
2059 <td><select name="updraft_interval_database">
2060 <?php
2061 $intervals = array ("manual", "daily", "weekly", "monthly");
2062 foreach ($intervals as $ival) {
2063 echo "<option value=\"$ival\" ";
2064 if ($ival == get_option('updraft_interval_database',get_option('updraft_interval'))) { echo 'selected="selected"';}
2065 echo ">".ucfirst($ival)."</option>\n";
2066 }
2067 ?>
2068 </select></td>
2069 </tr>
2070 <tr class="backup-interval-description">
2071 <td></td><td>If you would like to automatically schedule backups, choose schedules from the dropdown above. Backups will occur at the interval specified starting just after the current time. If you choose manual you must click the &quot;Backup Now!&quot; button whenever you wish a backup to occur. If the two schedules are the same, then the two backups will take place together.</td>
2072 </tr>
2073 <?php
2074 # The true (default value if non-existent) here has the effect of forcing a default of on.
2075 $include_themes = (get_option('updraft_include_themes',true)) ? 'checked="checked"' : "";
2076 $include_plugins = (get_option('updraft_include_plugins',true)) ? 'checked="checked"' : "";
2077 $include_uploads = (get_option('updraft_include_uploads',true)) ? 'checked="checked"' : "";
2078 $include_others = (get_option('updraft_include_others',true)) ? 'checked="checked"' : "";
2079 $include_others_exclude = get_option('updraft_include_others_exclude',UPDRAFT_DEFAULT_OTHERS_EXCLUDE);
2080 ?>
2081 <tr>
2082 <th>Include in Files Backup:</th>
2083 <td>
2084 <input type="checkbox" name="updraft_include_plugins" value="1" <?php echo $include_plugins; ?> /> Plugins<br />
2085 <input type="checkbox" name="updraft_include_themes" value="1" <?php echo $include_themes; ?> /> Themes<br />
2086 <input type="checkbox" name="updraft_include_uploads" value="1" <?php echo $include_uploads; ?> /> Uploads<br />
2087 <input type="checkbox" name="updraft_include_others" value="1" <?php echo $include_others; ?> /> Any other directories found inside wp-content - but exclude these directories: <input type="text" name="updraft_include_others_exclude" size="32" value="<?php echo htmlspecialchars($include_others_exclude); ?>"/><br />
2088 Include all of these, unless you are backing them up separately. Note that presently UpdraftPlus backs up these directories only - which is usually everything (except for WordPress core itself which you can download afresh from WordPress.org). But if you have made customised modifications outside of these directories, you need to back them up another way.<br />(<a href="http://wordshell.net">Use WordShell</a> for automatic backup, version control and patching).<br /></td>
2089 </td>
2090 </tr>
2091 <tr>
2092 <th>Retain Backups:</th>
2093 <?php
2094 $updraft_retain = get_option('updraft_retain');
2095 $retain = ((int)$updraft_retain > 0)?get_option('updraft_retain'):1;
2096 ?>
2097 <td><input type="text" name="updraft_retain" value="<?php echo $retain ?>" style="width:50px" /></td>
2098 </tr>
2099 <tr class="backup-retain-description">
2100 <td></td><td>By default only the most recent backup is retained. If you'd like to preserve more, specify the number here. (This many of <strong>both</strong> files and database backups will be retained.)</td>
2101 </tr>
2102 <tr>
2103 <th>Database encryption phrase:</th>
2104 <?php
2105 $updraft_encryptionphrase = get_option('updraft_encryptionphrase');
2106 ?>
2107 <td><input type="text" name="updraft_encryptionphrase" value="<?php echo $updraft_encryptionphrase ?>" style="width:132px" /></td>
2108 </tr>
2109 <tr class="backup-crypt-description">
2110 <td></td><td>If you enter a string here, it is used to encrypt backups (Rijndael). Do not lose it, or all your backups will be useless. Presently, only the database file is encrypted. This is also the key used to decrypt backups from this admin interface (so if you change it, then automatic decryption will not work until you change it back). You can also use the file example-decrypt.php from inside the UpdraftPlus plugin directory to decrypt manually.</td>
2111 </tr>
2112
2113 <tr>
2114 <th>Remote backup:</th>
2115 <td><select name="updraft_service" id="updraft-service">
2116 <?php
2117 $delete_local = (get_option('updraft_delete_local')) ? 'checked="checked"' : "";
2118 $debug_mode = (get_option('updraft_debug_mode')) ? 'checked="checked"' : "";
2119
2120 $display_none = 'style="display:none"';
2121 $s3 = ""; $ftp = ""; $email = ""; $googledrive="";
2122 $email_display="";
2123 $display_email_complete = "";
2124 $set = 'selected="selected"';
2125 switch(get_option('updraft_service')) {
2126 case 's3':
2127 $s3 = $set;
2128 $googledrive_display = $display_none;
2129 $ftp_display = $display_none;
2130 break;
2131 case 'googledrive':
2132 $googledrive = $set;
2133 $s3_display = $display_none;
2134 $ftp_display = $display_none;
2135 break;
2136 case 'ftp':
2137 $ftp = $set;
2138 $googledrive_display = $display_none;
2139 $s3_display = $display_none;
2140 break;
2141 case 'email':
2142 $email = $set;
2143 $ftp_display = $display_none;
2144 $s3_display = $display_none;
2145 $googledrive_display = $display_none;
2146 $display_email_complete = $display_none;
2147 break;
2148 default:
2149 $none = $set;
2150 $ftp_display = $display_none;
2151 $googledrive_display = $display_none;
2152 $s3_display = $display_none;
2153 $display_delete_local = $display_none;
2154 break;
2155 }
2156 ?>
2157 <option value="none" <?php echo $none?>>None</option>
2158 <option value="s3" <?php echo $s3?>>Amazon S3</option>
2159 <option value="googledrive" <?php echo $googledrive?>>Google Drive (experimental, may work for you, may not)</option>
2160 <option value="ftp" <?php echo $ftp?>>FTP</option>
2161 <option value="email" <?php echo $email?>>E-mail</option>
2162 </select></td>
2163 </tr>
2164 <tr class="backup-service-description">
2165 <td></td><td>Choose your backup method. Be aware that mail servers tend to have strict file size limitations; typically around 10-20Mb; backups larger than this may not arrive. Select none if you do not wish to send your backups anywhere <b>(not recommended)</b>.</td>
2166
2167 </tr>
2168
2169 <!-- Amazon S3 -->
2170 <tr class="s3" <?php echo $s3_display?>>
2171 <th>S3 access key:</th>
2172 <td><input type="text" autocomplete="off" style="width:292px" name="updraft_s3_login" value="<?php echo get_option('updraft_s3_login') ?>" /></td>
2173 </tr>
2174 <tr class="s3" <?php echo $s3_display?>>
2175 <th>S3 secret key:</th>
2176 <td><input type="password" autocomplete="off" style="width:292px" name="updraft_s3_pass" value="<?php echo get_option('updraft_s3_pass'); ?>" /></td>
2177 </tr>
2178 <tr class="s3" <?php echo $s3_display?>>
2179 <th>S3 location:</th>
2180 <td>s3://<input type="text" style="width:292px" name="updraft_s3_remote_path" value="<?php echo get_option('updraft_s3_remote_path'); ?>" /></td>
2181 </tr>
2182 <tr class="s3" <?php echo $s3_display?>>
2183 <th></th>
2184 <td><p>Get your access key and secret key from your AWS page, then pick a (globally unique) bucket name (letters and numbers) (and optionally a path) to use for storage.</p></td>
2185 </tr>
2186
2187 <!-- Google Drive -->
2188 <tr class="googledrive" <?php echo $googledrive_display?>>
2189 <th>Google Drive Client ID:</th>
2190 <td><input type="text" autocomplete="off" style="width:332px" name="updraft_googledrive_clientid" value="<?php echo get_option('updraft_googledrive_clientid') ?>" /></td>
2191 </tr>
2192 <tr class="googledrive" <?php echo $googledrive_display?>>
2193 <th>Google Drive Client Secret:</th>
2194 <td><input type="password" autocomplete="off" style="width:332px" name="updraft_googledrive_secret" value="<?php echo get_option('updraft_googledrive_secret'); ?>" /></td>
2195 </tr>
2196 <tr class="googledrive" <?php echo $googledrive_display?>>
2197 <th>Google Drive Folder ID:</th>
2198 <td><input type="text" style="width:332px" name="updraft_googledrive_remotepath" value="<?php echo get_option('updraft_googledrive_remotepath'); ?>" /> <em>(Leave empty to use your root folder)</em></td>
2199 </tr>
2200 <tr class="googledrive" <?php echo $googledrive_display?>>
2201 <th>Authenticate with Google:</th>
2202 <td><p><a href="?page=updraftplus&action=auth&updraftplus_googleauth=doit"><strong>After</strong> you have saved your settings (by clicking &quot;Save Changes&quot; below), then come back here once and click this link to complete authentication with Google.</a>
2203
2204 <?php
2205 if (get_option('updraft_googledrive_token','xyz') != 'xyz') {
2206 echo " (You appear to be already authenticated)";
2207 }
2208 ?>
2209 </p>
2210 <p>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></p>
2211 <p><strong>N.B. : If you choose Google Drive, then no backups will be deleted - all will be retained. Patches welcome!</strong></p>
2212 </td>
2213 </tr>
2214 <tr class="googledrive" <?php echo $googledrive_display?>>
2215 <th></th>
2216 <td><p>Create a Client ID in the API Access section of your <a href="https://code.google.com/apis/console/">Google API Console</a>. Select 'Web Application' as the application type.</p><p>You must add <kbd><?php echo admin_url('options-general.php?page=updraftplus&action=auth'); ?></kbd> as the authorised redirect URI when asked.</p>
2217 <?php
2218 if (!class_exists('SimpleXMLElement')) { echo "<p><b>WARNING:</b> You do not have SimpleXMLElement installed. Google Drive backups will <b>not</b> work until you do.</p>"; }
2219 ?>
2220 </td>
2221 </tr>
2222
2223 <tr class="ftp" <?php echo $ftp_display?>>
2224 <th><a href="#" title="Click for help!" onclick="jQuery('.ftp-description').toggle();return false;">FTP Server:</a></th>
2225 <td><input type="text" style="width:260px" name="updraft_server_address" value="<?php echo get_option('updraft_server_address'); ?>" /></td>
2226 </tr>
2227 <tr class="ftp" <?php echo $ftp_display?>>
2228 <th><a href="#" title="Click for help!" onclick="jQuery('.ftp-description').toggle();return false;">FTP Login:</a></th>
2229 <td><input type="text" autocomplete="off" name="updraft_ftp_login" value="<?php echo get_option('updraft_ftp_login') ?>" /></td>
2230 </tr>
2231 <tr class="ftp" <?php echo $ftp_display?>>
2232 <th><a href="#" title="Click for help!" onclick="jQuery('.ftp-description').toggle();return false;">FTP Password:</a></th>
2233 <td><input type="password" autocomplete="off" style="width:260px" name="updraft_ftp_pass" value="<?php echo get_option('updraft_ftp_pass'); ?>" /></td>
2234 </tr>
2235 <tr class="ftp" <?php echo $ftp_display?>>
2236 <th><a href="#" title="Click for help!" onclick="jQuery('.ftp-description').toggle();return false;">Remote Path:</a></th>
2237 <td><input type="text" style="width:260px" name="updraft_ftp_remote_path" value="<?php echo get_option('updraft_ftp_remote_path'); ?>" /></td>
2238 </tr>
2239 <tr class="ftp-description" style="display:none">
2240 <td colspan="2">An FTP remote path will look like '/home/backup/some/folder'</td>
2241 </tr>
2242 <tr class="email" <?php echo $email_display?>>
2243 <th>Email:</th>
2244 <td><input type="text" style="width:260px" name="updraft_email" value="<?php echo get_option('updraft_email'); ?>" /> <br />Enter an address here to have a report sent (and the whole backup, if you choose) to it.</td>
2245 </tr>
2246 <tr class="deletelocal s3 ftp email" <?php echo $display_delete_local?>>
2247 <th>Delete local backup:</th>
2248 <td><input type="checkbox" name="updraft_delete_local" value="1" <?php echo $delete_local; ?> /> <br />Check this to delete the local backup file (only sensible if you have enabled a remote backup, otherwise you will have no backup remaining).</td>
2249 </tr>
2250 <tr>
2251 <th>Debug mode:</th>
2252 <td><input type="checkbox" name="updraft_debug_mode" value="1" <?php echo $debug_mode; ?> /> <br />Check this for more information, if something is going wrong. Will also drop a log file in your backup directory which you can examine.</td>
2253 </tr>
2254 <tr>
2255 <td>
2256 <input type="hidden" name="action" value="update" />
2257 <input type="submit" class="button-primary" value="Save Changes" />
2258 </td>
2259 </tr>
2260 </table>
2261 </form>
2262 <?php
2263 if(get_option('updraft_debug_mode')) {
2264 ?>
2265 <div>
2266 <h3>Debug Information</h3>
2267 <?php
2268 $peak_memory_usage = memory_get_peak_usage(true)/1024/1024;
2269 $memory_usage = memory_get_usage(true)/1024/1024;
2270 echo 'Peak memory usage: '.$peak_memory_usage.' MB<br/>';
2271 echo 'Current memory usage: '.$memory_usage.' MB<br/>';
2272 echo 'PHP memory limit: '.ini_get('memory_limit').' <br/>';
2273 ?>
2274 <form method="post" action="">
2275 <input type="hidden" name="action" value="updraft_backup_debug_all" />
2276 <p><input type="submit" class="button-primary" <?php echo $backup_disabled ?> value="Debug Backup" onclick="return(confirm('This will cause an immediate backup. The page will stall loading until it finishes (ie, unscheduled). Use this if you\'re trying to see peak memory usage.'))" /></p>
2277 </form>
2278 <form method="post" action="">
2279 <input type="hidden" name="action" value="updraft_backup_debug_db" />
2280 <p><input type="submit" class="button-primary" <?php echo $backup_disabled ?> value="Debug DB Backup" onclick="return(confirm('This will cause an immediate DB backup. The page will stall loading until it finishes (ie, unscheduled). The backup will remain locally despite your prefs and will not go into the backup history or up into the cloud.'))" /></p>
2281 </form>
2282 </div>
2283 <?php } ?>
2284 <script type="text/javascript">
2285 jQuery(document).ready(function() {
2286 jQuery('#updraft-service').change(function() {
2287 switch(jQuery(this).val()) {
2288 case 'none':
2289 jQuery('.deletelocal,.s3,.ftp,.googledrive,.s3-description,.ftp-description').hide()
2290 jQuery('.email,.email-complete').show()
2291 break;
2292 case 's3':
2293 jQuery('.ftp,.ftp-description,.googledrive').hide()
2294 jQuery('.s3,.deletelocal,.email,.email-complete').show()
2295 break;
2296 case 'googledrive':
2297 jQuery('.ftp,.ftp-description,.s3').hide()
2298 jQuery('.googledrive,.deletelocal,.googledrive,.email,.email-complete').show()
2299 break;
2300 case 'ftp':
2301 jQuery('.googledrive,.s3,.s3-description').hide()
2302 jQuery('.ftp,.deletelocal,.email,.email-complete').show()
2303 break;
2304 case 'email':
2305 jQuery('.s3,.ftp,.s3-description,.googledrive,.ftp-description,.email-complete').hide()
2306 jQuery('.email,.deletelocal').show()
2307 break;
2308 }
2309 })
2310 })
2311 jQuery(window).load(function() {
2312 //this is for hiding the restore progress at the top after it is done
2313 setTimeout('jQuery("#updraft-restore-progress").toggle(1000)',3000)
2314 jQuery('#updraft-restore-progress-toggle').click(function() {
2315 jQuery('#updraft-restore-progress').toggle(500)
2316 })
2317 })
2318 </script>
2319 <?php
2320 }
2321
2322 /*array2json provided by bin-co.com under BSD license*/
2323 function array2json($arr) {
2324 if(function_exists('json_encode')) return stripslashes(json_encode($arr)); //Latest versions of PHP already have this functionality.
2325 $parts = array();
2326 $is_list = false;
2327
2328 //Find out if the given array is a numerical array
2329 $keys = array_keys($arr);
2330 $max_length = count($arr)-1;
2331 if(($keys[0] == 0) and ($keys[$max_length] == $max_length)) {//See if the first key is 0 and last key is length - 1
2332 $is_list = true;
2333 for($i=0; $i<count($keys); $i++) { //See if each key correspondes to its position
2334 if($i != $keys[$i]) { //A key fails at position check.
2335 $is_list = false; //It is an associative array.
2336 break;
2337 }
2338 }
2339 }
2340
2341 foreach($arr as $key=>$value) {
2342 if(is_array($value)) { //Custom handling for arrays
2343 if($is_list) $parts[] = $this->array2json($value); /* :RECURSION: */
2344 else $parts[] = '"' . $key . '":' . $this->array2json($value); /* :RECURSION: */
2345 } else {
2346 $str = '';
2347 if(!$is_list) $str = '"' . $key . '":';
2348
2349 //Custom handling for multiple data types
2350 if(is_numeric($value)) $str .= $value; //Numbers
2351 elseif($value === false) $str .= 'false'; //The booleans
2352 elseif($value === true) $str .= 'true';
2353 else $str .= '"' . addslashes($value) . '"'; //All other things
2354 // :TODO: Is there any more datatype we should be in the lookout for? (Object?)
2355
2356 $parts[] = $str;
2357 }
2358 }
2359 $json = implode(',',$parts);
2360
2361 if($is_list) return '[' . $json . ']';//Return numerical JSON
2362 return '{' . $json . '}';//Return associative JSON
2363 }
2364
2365 function show_admin_warning($message) {
2366 echo '<div id="updraftmessage" class="updated fade">';
2367 echo "<p>$message</p></div>";
2368 }
2369 function show_admin_warning_accessible() {
2370 $this->show_admin_warning("UpdraftPlus backup directory specified is accessible via the web. This is a potential security problem (people may be able to download your backups - which is undesirable if your database is not encrypted and if you have non-public assets amongst the files). If using Apache, enable .htaccess support to allow web access to be denied; otherwise, you should deny access manually.");
2371 }
2372 function show_admin_warning_googledrive() {
2373 $this->show_admin_warning('UpdraftPlus notice: <a href="?page=updraftplus&action=auth&updraftplus_googleauth=doit">Click here to authenticate your Google Drive account (you will not be able to back up to Google Drive without it).</a>');
2374 }
2375 function show_admin_warning_accessible_unknownresult() {
2376 $this->show_admin_warning("UpdraftPlus tried to check if the backup directory is accessible via web, but the result was unknown.");
2377 }
2378
2379
2380 }
2381
2382 ?>
2383