UpdraftPlus WordPress Backup Plugin - Version 0.9.10

Version Description

  • 11/22/2012 =
  • Completed basic Google Drive support (thanks to Sorin Iclanzan, code taken from "Backup" plugin under GPLv3+); now supporting uploading, purging and restoring - i.e. full UpdraftPlus functionality
  • Licence change to GPLv3+ (from GPLv2+) to allow incorporating Sorin's code
  • Tidied/organised the settings screen further
Download this release

Release Info

Developer DavidAnderson
Plugin Icon 128x128 UpdraftPlus WordPress Backup Plugin
Version 0.9.10
Comparing to
See all releases

Code changes from version 0.9.2 to 0.9.10

Files changed (3) hide show
  1. includes/class-gdocs.php +627 -0
  2. readme.txt +10 -5
  3. updraftplus.php +304 -345
includes/class-gdocs.php ADDED
@@ -0,0 +1,627 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ // Originally contained: GDocs class
4
+ // Contains: UpdraftPlus_GDocs class (new methods added - could not extend, as too much was private)
5
+
6
+ // The following copyright notice is reproduced exactly as found in the "Backup" plugin (http://wordpress.org/extend/plugins/backup)
7
+ // It applies to the code apart from the methods we added (get_content_link, download_data)
8
+
9
+ /*
10
+ Copyright 2012 Sorin Iclanzan (email : sorin@hel.io)
11
+
12
+ This file is part of Backup.
13
+
14
+ Backup is free software: you can redistribute it and/or modify
15
+ it under the terms of the GNU General Public License as published by
16
+ the Free Software Foundation, either version 3 of the License, or
17
+ (at your option) any later version.
18
+
19
+ Backup is distributed in the hope that it will be useful,
20
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
21
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22
+ GNU General Public License for more details.
23
+
24
+ You should have received a copy of the GNU General Public License
25
+ along with Backup. If not, see http://www.gnu.org/licenses/gpl.html.
26
+ */
27
+
28
+ /**
29
+ * Google Docs class
30
+ *
31
+ * Implements communication with Google Docs via the Google Documents List v3 API.
32
+ *
33
+ * Currently uploading, resuming and deleting resources is implemented as well as retrieving quotas.
34
+ *
35
+ * @uses WP_Error for storing error messages.
36
+ */
37
+ class UpdraftPlus_GDocs {
38
+
39
+ /**
40
+ * Stores the API version.
41
+ *
42
+ * @var string
43
+ * @access private
44
+ */
45
+ private $gdata_version;
46
+
47
+
48
+ /**
49
+ * Stores the base URL for the API requests.
50
+ *
51
+ * @var string
52
+ * @access private
53
+ */
54
+ private $base_url;
55
+
56
+ /**
57
+ * Stores the URL to the metadata feed.
58
+ * @var string
59
+ */
60
+ private $metadata_url;
61
+
62
+ /**
63
+ * Stores the token needed to access the API.
64
+ * @var string
65
+ * @access private
66
+ */
67
+ private $token;
68
+
69
+ /**
70
+ * Stores feeds to avoid requesting them again for successive use.
71
+ *
72
+ * @var array
73
+ * @access private
74
+ */
75
+ private $cache = array();
76
+
77
+ /**
78
+ * Files are uploadded in chunks of this size in bytes.
79
+ *
80
+ * @var integer
81
+ * @access private
82
+ */
83
+ private $chunk_size;
84
+
85
+ /**
86
+ * Stores whether or not to verify host SSL certificate.
87
+ *
88
+ * @var boolean
89
+ * @access private
90
+ */
91
+ private $ssl_verify;
92
+
93
+ /**
94
+ * Stores the number of seconds to wait for a response before timing out.
95
+ *
96
+ * @var integer
97
+ * @access private
98
+ */
99
+ private $request_timeout;
100
+
101
+ /**
102
+ * Stores the MIME type of the file that is uploading
103
+ *
104
+ * @var string
105
+ * @access private
106
+ */
107
+ private $upload_file_type;
108
+
109
+ /**
110
+ * Stores info about the file being uploaded.
111
+ *
112
+ * @var array
113
+ * @access private
114
+ */
115
+ private $file;
116
+
117
+ /**
118
+ * Stores the number of seconds the upload process is allowed to run
119
+ *
120
+ * @var integer
121
+ * @access private
122
+ */
123
+ private $time_limit;
124
+
125
+ /**
126
+ * Stores a timer for upload processes
127
+ *
128
+ * @var array
129
+ */
130
+ private $timer;
131
+
132
+ /**
133
+ * Constructor - Sets the access token.
134
+ *
135
+ * @param string $token Access token
136
+ */
137
+ function __construct( $token ) {
138
+ $this->token = $token;
139
+ $this->gdata_version = '3.0';
140
+ $this->base_url = 'https://docs.google.com/feeds/default/private/full/';
141
+ $this->metadata_url = 'https://docs.google.com/feeds/metadata/default';
142
+ $this->chunk_size = 524288; // 512 KiB
143
+ $this->max_resume_attempts = 5;
144
+ $this->request_timeout = 5;
145
+ $this->ssl_verify = true;
146
+ $this->timer = array(
147
+ 'start' => 0,
148
+ 'stop' => 0,
149
+ 'delta' => 0,
150
+ 'cycle' => 0
151
+ );
152
+ $this->time_limit = @ini_get( 'max_execution_time' );
153
+ if ( ! $this->time_limit && '0' !== $this->time_limit )
154
+ $this->time_limit = 30; // default php max exec time
155
+ }
156
+
157
+ /**
158
+ * Sets an option.
159
+ *
160
+ * @access public
161
+ * @param string $option The option to set.
162
+ * @param mixed $value The value to set the option to.
163
+ */
164
+ public function set_option( $option, $value ) {
165
+ switch ( $option ) {
166
+ case 'chunk_size':
167
+ if ( floatval($value) >= 0.5 ) {
168
+ $this->chunk_size = floatval($value) * 1024 * 1024; // Transform from MiB to bytes
169
+ return true;
170
+ }
171
+ break;
172
+ case 'ssl_verify':
173
+ $this->ssl_verify = ( bool ) $value;
174
+ return true;
175
+ case 'request_timeout':
176
+ if ( intval( $value ) > 0 ) {
177
+ $this->request_timeout = intval( $value );
178
+ return true;
179
+ }
180
+ break;
181
+ case 'max_resume_attempts':
182
+ $this->max_resume_attempts = intval($value);
183
+ return true;
184
+ }
185
+ return false;
186
+ }
187
+
188
+ /**
189
+ * Gets an option.
190
+ *
191
+ * @access public
192
+ * @param string $option The option to get.
193
+ */
194
+ public function get_option( $option ) {
195
+ switch ( $option ) {
196
+ case 'chunk_size':
197
+ return $this->chunk_size;
198
+ case 'ssl_verify':
199
+ return $this->ssl_verify;
200
+ case 'request_timeout':
201
+ return $this->request_timeout;
202
+ case 'max_resume_attempts':
203
+ return $this->max_resume_attempts;
204
+ }
205
+ return false;
206
+ }
207
+
208
+ /**
209
+ * This function makes all the requests to the API.
210
+ *
211
+ * @uses wp_remote_request
212
+ * @access private
213
+ * @param string $url The URL where the request is sent.
214
+ * @param string $method The HTTP request method, defaults to 'GET'.
215
+ * @param array $headers Headers to be sent.
216
+ * @param string $body The body of the request.
217
+ * @return mixed Returns an array containing the response on success or an instance of WP_Error on failure.
218
+ */
219
+ private function request( $url, $method = 'GET', $headers = array(), $body = NULL ) {
220
+ $args = array(
221
+ 'method' => $method,
222
+ 'timeout' => $this->request_timeout,
223
+ 'httpversion' => '1.1',
224
+ 'redirection' => 0,
225
+ 'sslverify' => $this->ssl_verify,
226
+ 'headers' => array(
227
+ 'Authorization' => 'Bearer ' . $this->token,
228
+ 'GData-Version' => $this->gdata_version
229
+ )
230
+ );
231
+ if ( ! empty( $headers ) )
232
+ $args['headers'] = array_merge( $args['headers'], $headers );
233
+ if ( ! empty( $body ) )
234
+ $args['body'] = $body;
235
+
236
+ return wp_remote_request( $url, $args );
237
+ }
238
+
239
+ /**
240
+ * Returns the feed from a URL.
241
+ *
242
+ * @access public
243
+ * @param string $url The feed URL.
244
+ * @return mixed Returns the feed as an instance of SimpleXMLElement on success or an instance of WP_Error on failure.
245
+ */
246
+ public function get_feed( $url ) {
247
+ if ( ! isset( $this->cache[$url] ) ) {
248
+ $result = $this->cache_feed( $url );
249
+ if ( is_wp_error( $result ) )
250
+ return $result;
251
+ }
252
+
253
+ return $this->cache[$url];
254
+ }
255
+
256
+ /**
257
+ * Requests a feed and adds it to cache.
258
+ *
259
+ * @access private
260
+ * @param string $url The feed URL.
261
+ * @return mixed Returns TRUE on success or an instance of WP_Error on failure.
262
+ */
263
+ private function cache_feed( $url ) {
264
+ $result = $this->request( $url );
265
+
266
+ if ( is_wp_error( $result ) )
267
+ return $result;
268
+
269
+ if ( $result['response']['code'] == '200' ) {
270
+ $feed = @simplexml_load_string( $result['body'] );
271
+ if ( $feed === false )
272
+ return new WP_Error( 'invalid_data', "Could not create SimpleXMLElement from '" . $result['body'] . "'." );
273
+
274
+ $this->cache[$url] = $feed;
275
+ return true;
276
+ }
277
+ return new WP_Error( 'bad_response', "Received response code '" . $result['response']['code'] . " " . $result['response']['message'] . "' while trying to get '" . $url . "'. Response body: " . $result['body'] );
278
+
279
+ }
280
+
281
+ /**
282
+ * Deletes a resource from Google Docs.
283
+ *
284
+ * @access public
285
+ * @param string $id Gdata Id of the resource to be deleted.
286
+ * @return mixed Returns TRUE on success, an instance of WP_Error on failure.
287
+ */
288
+ public function delete_resource( $id ) {
289
+ $headers = array( 'If-Match' => '*' );
290
+
291
+ $result = $this->request( $this->base_url . $id . '?delete=true', 'DELETE', $headers );
292
+ if ( is_wp_error( $result ) )
293
+ return $result;
294
+
295
+ if ( $result['response']['code'] == '200' )
296
+ return true;
297
+ return new WP_Error( 'bad_response', "Received response code '" . $result['response']['code'] . " " . $result['response']['message'] . "' while trying to delete resource '" . $id . "'. The resource might not have been deleted." );
298
+ }
299
+
300
+ /**
301
+ * Get the resumable-create-media link needed to upload files.
302
+ *
303
+ * @access private
304
+ * @param string $parent The Id of the folder where the upload is to be made. Default is empty string.
305
+ * @return mixed Returns a link on success, instance of WP_Error on failure.
306
+ */
307
+ private function get_resumable_create_media_link( $parent = '' ) {
308
+ $url = $this->base_url;
309
+ if ( $parent )
310
+ $url .= $parent;
311
+
312
+ $feed = $this->get_feed( $url );
313
+
314
+ if ( is_wp_error( $feed ) )
315
+ return $feed;
316
+
317
+ foreach ( $feed->link as $link )
318
+ if ( $link['rel'] == 'http://schemas.google.com/g/2005#resumable-create-media' )
319
+ return ( string ) $link['href'];
320
+ return new WP_Error( 'not_found', "The 'resumable_create_media_link' was not found in feed." );
321
+ }
322
+
323
+ /**
324
+ * Get used quota in bytes.
325
+ *
326
+ * @access public
327
+ * @return mixed Returns the number of bytes used in Google Docs on success or an instance of WP_Error on failure.
328
+ */
329
+ public function get_quota_used() {
330
+ $feed = $this->get_feed( $this->metadata_url );
331
+ if ( is_wp_error( $feed ) )
332
+ return $feed;
333
+ return ( string ) $feed->children( "http://schemas.google.com/g/2005" )->quotaBytesUsed;
334
+ }
335
+
336
+ /**
337
+ * Get total quota in bytes.
338
+ *
339
+ * @access public
340
+ * @return string|WP_Error Returns the total quota in bytes in Google Docs on success or an instance of WP_Error on failure.
341
+ */
342
+ public function get_quota_total() {
343
+ $feed = $this->get_feed( $this->metadata_url );
344
+ if ( is_wp_error( $feed ) )
345
+ return $feed;
346
+ return ( string ) $feed->children( "http://schemas.google.com/g/2005" )->quotaBytesTotal;
347
+ }
348
+
349
+ /**
350
+ * Function to prepare a file to be uploaded to Google Docs.
351
+ *
352
+ * The function requests a URI for uploading and prepends a new element in the resume_list array.
353
+ *
354
+ * @uses wp_check_filetype
355
+ * @access public
356
+ *
357
+ * @param string $file Path to the file that is to be uploaded.
358
+ * @param string $title Title to be given to the file.
359
+ * @param string $parent ID of the folder in which to upload the file.
360
+ * @param string $type MIME type of the file to be uploaded. The function tries to identify the type if it is omitted.
361
+ * @return mixed Returns the URI where to upload on success, an instance of WP_Error on failure.
362
+ */
363
+ public function prepare_upload( $file, $title, $parent = '', $type = '' ) {
364
+ if ( ! @is_readable( $file ) )
365
+ return new WP_Error( 'not_file', "The path '" . $file . "' does not point to a readable file." );
366
+
367
+ // If a mime type wasn't passed try to guess it from the extension based on the WordPress allowed mime types
368
+ if ( empty( $type ) ) {
369
+ $check = wp_check_filetype( $file );
370
+ $this->upload_file_type = $type = $check['type'];
371
+ }
372
+
373
+ $size = filesize( $file );
374
+
375
+ $body = '<?xml version=\'1.0\' encoding=\'UTF-8\'?><entry xmlns="http://www.w3.org/2005/Atom" xmlns:docs="http://schemas.google.com/docs/2007"><category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/docs/2007#file"/><title>' . $title . '</title></entry>';
376
+
377
+ $headers = array(
378
+ 'Content-Type' => 'application/atom+xml',
379
+ 'X-Upload-Content-Type' => $type,
380
+ 'X-Upload-Content-Length' => (string) $size
381
+ );
382
+
383
+ $url = $this->get_resumable_create_media_link( $parent );
384
+
385
+ if ( is_wp_error( $url ) )
386
+ return $url;
387
+
388
+ $url .= '?convert=false'; // needed to upload a file
389
+
390
+ $result = $this->request( $url, 'POST', $headers, $body );
391
+
392
+ if ( is_wp_error( $result ) )
393
+ return $result;
394
+
395
+ if ( $result['response']['code'] != '200' )
396
+ return new WP_Error( 'bad_response', "Received response code '" . $result['response']['code'] . " " . $result['response']['message'] . "' while trying to get '" . $url . "'." );
397
+
398
+ $this->file = array(
399
+ 'path' => $file,
400
+ 'size' => $size,
401
+ 'location' => $result['headers']['location'],
402
+ 'pointer' => 0
403
+ );
404
+
405
+ // Open file for reading.
406
+ if ( !$this->file['handle'] = fopen( $file, "rb" ) )
407
+ return new WP_Error( 'open_error', "Could not open file '" . $file . "' for reading." );
408
+
409
+ // Start timer
410
+ $this->timer['start'] = microtime( true );
411
+
412
+ return $result['headers']['location'];
413
+ }
414
+
415
+
416
+ /**
417
+ * Resume an upload.
418
+ *
419
+ * @access public
420
+ * @param string $file Path to the file which needs to be uploaded
421
+ * @param string $location URI where to upload the file
422
+ * @return mixed Returns the next location URI on success, an instance of WP_Error on failure.
423
+ */
424
+ public function resume_upload( $file, $location ) {
425
+
426
+ if ( ! @is_readable( $file ) )
427
+ return new WP_Error( 'not_file', "The path '" . $this->resume_list[$id]['path'] . "' does not point to a readable file. Upload has been canceled." );
428
+
429
+ $size = filesize( $file );
430
+
431
+ $headers = array( 'Content-Range' => 'bytes */' . $size );
432
+ $result = $this->request( $location, 'PUT', $headers );
433
+ if( is_wp_error( $result ) )
434
+ return $result;
435
+
436
+ if ( '308' != $result['response']['code'] ) {
437
+ if ( '201' == $result['response']['code'] ) {
438
+ $feed = @simplexml_load_string( $result['body'] );
439
+ if ( $feed === false )
440
+ return new WP_Error( 'invalid_data', "Could not create SimpleXMLElement from '" . $result['body'] . "'." );
441
+ $this->file['id'] = substr( ( string ) $feed->children( "http://schemas.google.com/g/2005" )->resourceId, 5 );
442
+ return true;
443
+ }
444
+ return new WP_Error( 'bad_response', "Received response code '" . $result['response']['code'] . " " . $result['response']['message'] . "' while trying to resume the upload of file '" . $file . "'." );
445
+ }
446
+ if( isset( $result['headers']['location'] ) )
447
+ $location = $result['headers']['location'];
448
+ $pointer = $this->pointer( $result['headers']['range'] );
449
+
450
+ $this->file = array(
451
+ 'path' => $file,
452
+ 'size' => $size,
453
+ 'location' => $location,
454
+ 'pointer' => $pointer
455
+ );
456
+
457
+ // Open file for reading.
458
+ if ( !$this->file['handle'] = fopen( $file, "rb" ) )
459
+ return new WP_Error( 'open_error', "Could not open file '" . $file . "' for reading." );
460
+
461
+ // Start timer
462
+ $this->timer['start'] = microtime( true );
463
+
464
+ return $location;
465
+ }
466
+
467
+ /**
468
+ * Work out where the file pointer should be from the range header.
469
+ *
470
+ * @access private
471
+ * @param string $range The range HTTP response header.
472
+ * @return integer Returns the number of bytes that have been uploaded.
473
+ */
474
+ private function pointer( $range ) {
475
+ return intval(substr( $range, strpos( $range, '-' ) + 1 )) + 1;
476
+ }
477
+
478
+ /**
479
+ * Uploads a chunk of the file being uploaded.
480
+ *
481
+ * @access public
482
+ * @return mixed Returns TRUE if the chunk was uploaded successfully;
483
+ * returns Google Docs resource ID if the file upload finished;
484
+ * returns an instance of WP_Error on failure.
485
+ */
486
+ public function upload_chunk() {
487
+ if ( !isset( $this->file['handle'] ) )
488
+ return new WP_Error( "no_upload", "There is no file being uploaded." );
489
+
490
+ $cycle_start = microtime( true );
491
+ fseek( $this->file['handle'], $this->file['pointer'] );
492
+ $chunk = @fread( $this->file['handle'], $this->chunk_size );
493
+ if ( false === $chunk )
494
+ return new WP_Error( 'read_error', "Failed to read from file '" . $this->resume_list[$id]['path'] . "'." );
495
+
496
+ $chunk_size = strlen( $chunk );
497
+ $bytes = 'bytes ' . (string)$this->file['pointer'] . '-' . (string)($this->file['pointer'] + $chunk_size - 1) . '/' . (string)$this->file['size'];
498
+
499
+ $headers = array( 'Content-Range' => $bytes );
500
+
501
+ $result = $this->request( $this->file['location'], 'PUT', $headers, $chunk );
502
+
503
+ if ( !is_wp_error( $result ) )
504
+ if ( '308' == $result['response']['code'] ) {
505
+ if ( isset( $result['headers']['range'] ) )
506
+ $this->file['pointer'] = $this->pointer( $result['headers']['range'] );
507
+ else
508
+ $this->file['pointer'] += $chunk_size;
509
+
510
+ if ( isset( $result['headers']['location'] ) )
511
+ $this->file['location'] = $result['headers']['location'];
512
+
513
+ if ( $this->timer['cycle'] )
514
+ $this->timer['cycle'] = ( microtime( true ) - $cycle_start + $this->timer['cycle'] ) / 2;
515
+ else
516
+ $this->timer['cycle'] = microtime(true) - $cycle_start;
517
+
518
+ return $this->file['location'];
519
+ }
520
+ elseif ( '201' == $result['response']['code'] ) {
521
+ fclose( $this->file['handle'] );
522
+
523
+ // Stop timer
524
+ $this->timer['stop'] = microtime(true);
525
+ $this->timer['delta'] = $this->timer['stop'] - $this->timer['start'];
526
+
527
+ if ( $this->timer['cycle'] )
528
+ $this->timer['cycle'] = ( microtime( true ) - $cycle_start + $this->timer['cycle'] ) / 2;
529
+ else
530
+ $this->timer['cycle'] = microtime(true) - $cycle_start;
531
+
532
+ $this->file['pointer'] = $this->file['size'];
533
+
534
+ $feed = @simplexml_load_string( $result['body'] );
535
+ if ( $feed === false )
536
+ return new WP_Error( 'invalid_data', "Could not create SimpleXMLElement from '" . $result['body'] . "'." );
537
+ $this->file['id'] = substr( ( string ) $feed->children( "http://schemas.google.com/g/2005" )->resourceId, 5 );
538
+ return true;
539
+ }
540
+
541
+ // If we got to this point it means the upload wasn't successful.
542
+ fclose( $this->file['handle'] );
543
+ if ( is_wp_error( $result ) )
544
+ return $result;
545
+ return new WP_Error( 'bad_response', "Received response code '" . $result['response']['code'] . " " . $result['response']['message'] . "' while trying to upload a file chunk." );
546
+ }
547
+
548
+ /**
549
+ * Get the resource ID of the most recent uploaded file.
550
+ *
551
+ * @access public
552
+ * @return string The ID of the uploaded file or an empty string.
553
+ */
554
+ public function get_file_id() {
555
+ if ( isset( $this->file['id'] ) )
556
+ return $this->file['id'];
557
+ return '';
558
+ }
559
+
560
+ /**
561
+ * Get the upload speed recorded on the last upload performed.
562
+ *
563
+ * @access public
564
+ * @return integer Returns the upload speed in bytes/second or 0.
565
+ */
566
+ public function get_upload_speed() {
567
+ if ( $this->timer['cycle'] > 0 )
568
+ if ( $this->file['size'] < $this->chunk_size )
569
+ return $this->file['size'] / $this->timer['cycle'];
570
+ else
571
+ return $this->chunk_size / $this->timer['cycle'];
572
+ return 0;
573
+ }
574
+
575
+ /**
576
+ * Get the percentage of the file uploaded.
577
+ *
578
+ * @return float Returns a percentage on success, 0 on failure.
579
+ */
580
+ public function get_upload_percentage() {
581
+ if ( isset( $this->file['path'] ) )
582
+ return $this->file['pointer'] * 100 / $this->file['size'];
583
+ return 0;
584
+ }
585
+
586
+ /**
587
+ * Returns the time taken for an upload to complete.
588
+ *
589
+ * @access public
590
+ * @return float Returns the number of seconds the last upload took to complete, 0 if there has been no completed upload.
591
+ */
592
+ public function time_taken() {
593
+ return $this->timer['delta'];
594
+ }
595
+
596
+ public function get_content_link( $id, $title ) {
597
+
598
+ $feed = $this->get_feed($this->base_url . $id);
599
+
600
+ if ( is_wp_error( $feed ) )
601
+ return $feed;
602
+
603
+ if ( $feed->title != $title )
604
+ return new WP_Error( 'bad_response', "Unexpected response");
605
+
606
+ $att = $feed->content->attributes();
607
+ return $att['src'];
608
+
609
+ }
610
+
611
+ public function download_data( $link, $saveas ) {
612
+
613
+ $result = $this->request( $link );
614
+
615
+ if ( is_wp_error( $result ) )
616
+ return $result;
617
+
618
+ if ( $result['response']['code'] != '200' )
619
+ return new WP_Error( 'bad_response', "Received response code '" . $result['response']['code'] . " " . $result['response']['message'] . "' while trying to get '" . $url . "'." );
620
+
621
+ file_put_contents($saveas, $result['body']);
622
+
623
+ }
624
+
625
+
626
+
627
+ }
readme.txt CHANGED
@@ -3,12 +3,12 @@ Contributors: David Anderson
3
  Tags: backup, restore, database, cloud, amazon, s3, Amazon S3, google drive, google, gdrive, ftp, cloud, updraft, back up
4
  Requires at least: 3.2
5
  Tested up to: 3.5
6
- Stable tag: 0.9.2
7
  Donate link: http://david.dw-perspective.org.uk/donate
8
  License: GPLv3 or later
9
 
10
  == Upgrade Notice ==
11
- Failed uploads are now retried after 5 minutes, improving reliability
12
 
13
  == Description ==
14
 
@@ -70,19 +70,24 @@ Contact me! This is a complex plugin and the only way I can ensure it's robust i
70
 
71
  == Changelog ==
72
 
 
 
 
 
 
73
  = 0.9.2 - 11/21/2012 =
74
- * Failed uploads can now be resumed, giving really big blogs a better opportunity to eventually succeed uploading
75
 
76
  = 0.8.51 - 11/19/2012 =
77
  * Moved screenshot into assets, reducing plugin download size
78
 
79
  = 0.8.50 - 10/13/2012 =
80
- * Important new feature: back up other directories found in the WP content directory (not just plugins/themes/uploads, as in original Updraft)
81
 
82
  = 0.8.37 - 10/12/2012 =
83
  * Don't whinge about Google Drive authentication if that method is not current
84
 
85
- = 0.8.36 - 03/10/2012 =
86
  * Support using sub-directories in Amazon S3
87
  * Some more debug logging for Amazon S3
88
 
3
  Tags: backup, restore, database, cloud, amazon, s3, Amazon S3, google drive, google, gdrive, ftp, cloud, updraft, back up
4
  Requires at least: 3.2
5
  Tested up to: 3.5
6
+ Stable tag: 0.9.10
7
  Donate link: http://david.dw-perspective.org.uk/donate
8
  License: GPLv3 or later
9
 
10
  == Upgrade Notice ==
11
+ Full Google Drive support
12
 
13
  == Description ==
14
 
70
 
71
  == Changelog ==
72
 
73
+ = 0.9.10 - 11/22/2012 =
74
+ * Completed basic Google Drive support (thanks to Sorin Iclanzan, code taken from "Backup" plugin under GPLv3+); now supporting uploading, purging and restoring - i.e. full UpdraftPlus functionality
75
+ * Licence change to GPLv3+ (from GPLv2+) to allow incorporating Sorin's code
76
+ * Tidied/organised the settings screen further
77
+
78
  = 0.9.2 - 11/21/2012 =
79
+ * Failed uploads can now be re-tried, giving really big blogs a better opportunity to eventually succeed uploading
80
 
81
  = 0.8.51 - 11/19/2012 =
82
  * Moved screenshot into assets, reducing plugin download size
83
 
84
  = 0.8.50 - 10/13/2012 =
85
+ * Important new feature: back up other directories found in the WP content (wp-content) directory (not just plugins/themes/uploads, as in original Updraft)
86
 
87
  = 0.8.37 - 10/12/2012 =
88
  * Don't whinge about Google Drive authentication if that method is not current
89
 
90
+ = 0.8.36 - 10/03/2012 =
91
  * Support using sub-directories in Amazon S3
92
  * Some more debug logging for Amazon S3
93
 
updraftplus.php CHANGED
@@ -4,12 +4,14 @@ 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.2
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
@@ -21,7 +23,6 @@ Author URI: http://wordshell.net
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?
@@ -29,10 +30,12 @@ Encrypt filesystem, if memory allows (and have option for abort if not); split u
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,
@@ -59,7 +62,7 @@ define('UPDRAFT_DEFAULT_OTHERS_EXCLUDE','upgrade,cache,updraft,index.php');
59
 
60
  class UpdraftPlus {
61
 
62
- var $version = '0.9.2';
63
 
64
  var $dbhandle;
65
  var $errors = array();
@@ -67,7 +70,10 @@ class UpdraftPlus {
67
  var $logfile_name = "";
68
  var $logfile_handle = false;
69
  var $backup_time;
70
-
 
 
 
71
  function __construct() {
72
  // Initialisation actions
73
  # Create admin page
@@ -91,11 +97,11 @@ class UpdraftPlus {
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
  }
@@ -103,7 +109,7 @@ class UpdraftPlus {
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'),
@@ -149,254 +155,30 @@ class UpdraftPlus {
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(
@@ -439,7 +221,7 @@ class UpdraftPlus {
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' ) ) );
@@ -467,6 +249,7 @@ class UpdraftPlus {
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;
@@ -550,6 +333,7 @@ class UpdraftPlus {
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
@@ -678,10 +462,16 @@ class UpdraftPlus {
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) {
@@ -766,8 +556,8 @@ class UpdraftPlus {
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']);
@@ -810,8 +600,8 @@ class UpdraftPlus {
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
  }
@@ -862,28 +652,138 @@ class UpdraftPlus {
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');
@@ -1278,7 +1178,7 @@ class UpdraftPlus {
1278
  $row_start = $segment * ROWS_PER_SEGMENT;
1279
  $row_inc = ROWS_PER_SEGMENT;
1280
  }
1281
- do {
1282
  // don't include extra stuff, if so requested
1283
  $excs = array('revisions' => 0, 'spam' => 1); //TODO, FIX THIS
1284
  $where = '';
@@ -1349,16 +1249,8 @@ class UpdraftPlus {
1349
  }
1350
  }
1351
 
1352
- /**
1353
- * Logs any error messages
1354
- * @param array $args
1355
- * @return bool
1356
- */
1357
  function error($error,$severity='') {
1358
- $this->errors[] = array('error'=>$error,'severity'=>$severity);
1359
- if ($severity == 'fatal') {
1360
- //do something...
1361
- }
1362
  return true;
1363
  }
1364
 
@@ -1467,7 +1359,7 @@ class UpdraftPlus {
1467
  $len = filesize($fullpath);
1468
 
1469
  $filearr = explode('.',$file);
1470
- //we've only got zip and gz...for now
1471
  $file_ext = array_pop($filearr);
1472
  if($file_ext == 'zip') {
1473
  header('Content-type: application/zip');
@@ -1527,7 +1419,48 @@ class UpdraftPlus {
1527
  }
1528
 
1529
  function download_googledrive_backup($file) {
1530
- $this->error("Google Drive error: we do not yet support downloading existing backups from Google Drive - you need to restore the backup manually");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1531
  }
1532
 
1533
  function download_s3_backup($file) {
@@ -1552,9 +1485,8 @@ class UpdraftPlus {
1552
  }
1553
 
1554
  function download_ftp_backup($file) {
1555
- if( !class_exists('ftp_wrapper')) {
1556
- require_once(dirname(__FILE__).'/includes/ftp.class.php');
1557
- }
1558
  //handle SSL and errors at some point TODO
1559
  $ftp = new ftp_wrapper(get_option('updraft_server_address'),get_option('updraft_ftp_login'),get_option('updraft_ftp_pass'));
1560
  $ftp->passive = true;
@@ -1779,11 +1711,6 @@ class UpdraftPlus {
1779
 
1780
  function settings_output() {
1781
 
1782
- $ws_advert = $this->wordshell_random_advert(1);
1783
- echo <<<ENDHERE
1784
- <div class="updated fade" style="font-size:140%; padding:14px;">${ws_advert}</div>
1785
- ENDHERE;
1786
-
1787
  /*
1788
  we use request here because the initial restore is triggered by a POSTed form. we then may need to obtain credentials
1789
  for the WP_Filesystem. to do this WP outputs a form that we can't insert variables into (apparently). So the values are
@@ -1796,8 +1723,15 @@ ENDHERE;
1796
  echo '<b>Actions:</b> <a href="options-general.php?page=updraftplus&updraft_restore_success=true">Return to Updraft Configuration</a>.';
1797
  return;
1798
  } else {
1799
- echo '<p>Restore failed...</p><br/>';
1800
- echo '<b>Actions:</b> <a href="options-general.php?page=updraftplus">Return to Updraft Configuration</a>.';
 
 
 
 
 
 
 
1801
  return;
1802
  }
1803
  //uncomment the below once i figure out how i want the flow of a restoration to work.
@@ -1832,11 +1766,13 @@ ENDHERE;
1832
  }
1833
 
1834
  if(isset($_POST['action']) && $_POST['action'] == 'updraft_backup') {
 
1835
  if (wp_schedule_single_event(time()+5, 'updraft_backup_all') === false) {
1836
- echo "<!-- updraft schedule: failed -->";
1837
  } else {
1838
- echo "<!-- updraft schedule: ok -->";
1839
  }
 
1840
  }
1841
  if(isset($_POST['action']) && $_POST['action'] == 'updraft_backup_debug_all') {
1842
  $this->backup(true,true);
@@ -1847,19 +1783,24 @@ ENDHERE;
1847
 
1848
  ?>
1849
  <div class="wrap">
1850
- <h2>UpdraftPlus - Backup/Restore</h2>
1851
 
1852
- Version: <b><?php echo $this->version; ?></b><br />
1853
- 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> | <a href="http://profiles.wordpress.org/davidanderson/">My other WordPress plugins</a>)
1854
- <br />
1855
- 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> )
1856
- <br />
1857
  <?php
1858
  if(isset($_GET['updraft_restore_success'])) {
1859
  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>";
1860
  }
 
 
 
 
 
 
 
1861
  if($deleted_old_dirs) {
1862
- echo "<div style=\"color:blue\">Old directories successfully deleted.</div>";
1863
  }
1864
  if(!$this->memory_check(96)) {?>
1865
  <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>
@@ -1885,7 +1826,9 @@ ENDHERE;
1885
  }
1886
  }
1887
  ?>
1888
- <table class="form-table" style="float:left;width:475px">
 
 
1889
  <tr>
1890
  <?php
1891
  $next_scheduled_backup = wp_next_scheduled('updraft_backup');
@@ -1920,7 +1863,8 @@ ENDHERE;
1920
  $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>';
1921
  }
1922
  ?>
1923
- <th>Now:</th>
 
1924
  <td style="color:blue"><?php echo $current_time?></td>
1925
  </tr>
1926
  <tr>
@@ -1936,10 +1880,10 @@ ENDHERE;
1936
  <td style="color:<?php echo $last_backup_color ?>"><?php echo $last_backup?></td>
1937
  </tr>
1938
  </table>
1939
- <div style="float:left;width:200px">
1940
  <form method="post" action="">
1941
  <input type="hidden" name="action" value="updraft_backup" />
1942
- <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>
1943
  </form>
1944
  <div style="position:relative">
1945
  <div style="position:absolute;top:0;left:0">
@@ -2040,14 +1984,8 @@ ENDHERE;
2040
  </table>
2041
  <form method="post" action="options.php">
2042
  <?php settings_fields('updraft-options-group'); ?>
2043
- <table class="form-table">
2044
- <tr>
2045
- <th>Backup Directory:</th>
2046
- <td><input type="text" name="updraft_dir" style="width:525px" value="<?php echo $updraft_dir ?>" /></td>
2047
- </tr>
2048
- <tr>
2049
- <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>
2050
- </tr>
2051
  <tr>
2052
  <th>File Backup Intervals:</th>
2053
  <td><select name="updraft_interval">
@@ -2088,11 +2026,11 @@ ENDHERE;
2088
  <tr>
2089
  <th>Include in Files Backup:</th>
2090
  <td>
2091
- <input type="checkbox" name="updraft_include_plugins" value="1" <?php echo $include_plugins; ?> /> Plugins<br />
2092
- <input type="checkbox" name="updraft_include_themes" value="1" <?php echo $include_themes; ?> /> Themes<br />
2093
- <input type="checkbox" name="updraft_include_uploads" value="1" <?php echo $include_uploads; ?> /> Uploads<br />
2094
- <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 />
2095
- 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>
2096
  </td>
2097
  </tr>
2098
  <tr>
@@ -2103,6 +2041,15 @@ ENDHERE;
2103
  ?>
2104
  <td><input type="text" name="updraft_retain" value="<?php echo $retain ?>" style="width:50px" /></td>
2105
  </tr>
 
 
 
 
 
 
 
 
 
2106
  <tr class="backup-retain-description">
2107
  <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>
2108
  </tr>
@@ -2116,7 +2063,11 @@ ENDHERE;
2116
  <tr class="backup-crypt-description">
2117
  <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>
2118
  </tr>
 
 
 
2119
 
 
2120
  <tr>
2121
  <th>Remote backup:</th>
2122
  <td><select name="updraft_service" id="updraft-service">
@@ -2163,13 +2114,13 @@ ENDHERE;
2163
  ?>
2164
  <option value="none" <?php echo $none?>>None</option>
2165
  <option value="s3" <?php echo $s3?>>Amazon S3</option>
2166
- <option value="googledrive" <?php echo $googledrive?>>Google Drive (experimental, may work for you, may not)</option>
2167
  <option value="ftp" <?php echo $ftp?>>FTP</option>
2168
  <option value="email" <?php echo $email?>>E-mail</option>
2169
  </select></td>
2170
  </tr>
2171
  <tr class="backup-service-description">
2172
- <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>
2173
 
2174
  </tr>
2175
 
@@ -2202,7 +2153,7 @@ ENDHERE;
2202
  </tr>
2203
  <tr class="googledrive" <?php echo $googledrive_display?>>
2204
  <th>Google Drive Folder ID:</th>
2205
- <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>
2206
  </tr>
2207
  <tr class="googledrive" <?php echo $googledrive_display?>>
2208
  <th>Authenticate with Google:</th>
@@ -2214,16 +2165,17 @@ ENDHERE;
2214
  }
2215
  ?>
2216
  </p>
2217
- <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>
2218
- <p><strong>N.B. : If you choose Google Drive, then no backups will be deleted - all will be retained. Patches welcome!</strong></p>
2219
  </td>
2220
  </tr>
2221
  <tr class="googledrive" <?php echo $googledrive_display?>>
2222
  <th></th>
2223
- <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>
 
 
2224
  <?php
2225
- 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>"; }
2226
  ?>
 
2227
  </td>
2228
  </tr>
2229
 
@@ -2246,17 +2198,19 @@ ENDHERE;
2246
  <tr class="ftp-description" style="display:none">
2247
  <td colspan="2">An FTP remote path will look like '/home/backup/some/folder'</td>
2248
  </tr>
2249
- <tr class="email" <?php echo $email_display?>>
2250
- <th>Email:</th>
2251
- <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>
 
 
 
2252
  </tr>
2253
- <tr class="deletelocal s3 ftp email" <?php echo $display_delete_local?>>
2254
- <th>Delete local backup:</th>
2255
- <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>
2256
  </tr>
2257
  <tr>
2258
  <th>Debug mode:</th>
2259
- <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>
2260
  </tr>
2261
  <tr>
2262
  <td>
@@ -2269,7 +2223,8 @@ ENDHERE;
2269
  <?php
2270
  if(get_option('updraft_debug_mode')) {
2271
  ?>
2272
- <div>
 
2273
  <h3>Debug Information</h3>
2274
  <?php
2275
  $peak_memory_usage = memory_get_peak_usage(true)/1024/1024;
@@ -2288,29 +2243,33 @@ ENDHERE;
2288
  </form>
2289
  </div>
2290
  <?php } ?>
 
 
 
 
2291
  <script type="text/javascript">
2292
  jQuery(document).ready(function() {
2293
  jQuery('#updraft-service').change(function() {
2294
  switch(jQuery(this).val()) {
2295
  case 'none':
2296
- jQuery('.deletelocal,.s3,.ftp,.googledrive,.s3-description,.ftp-description').hide()
2297
- jQuery('.email,.email-complete').show()
2298
  break;
2299
  case 's3':
2300
- jQuery('.ftp,.ftp-description,.googledrive').hide()
2301
- jQuery('.s3,.deletelocal,.email,.email-complete').show()
2302
  break;
2303
  case 'googledrive':
2304
- jQuery('.ftp,.ftp-description,.s3').hide()
2305
- jQuery('.googledrive,.deletelocal,.googledrive,.email,.email-complete').show()
2306
  break;
2307
  case 'ftp':
2308
- jQuery('.googledrive,.s3,.s3-description').hide()
2309
- jQuery('.ftp,.deletelocal,.email,.email-complete').show()
2310
  break;
2311
  case 'email':
2312
- jQuery('.s3,.ftp,.s3-description,.googledrive,.ftp-description,.email-complete').hide()
2313
- jQuery('.email,.deletelocal').show()
2314
  break;
2315
  }
2316
  })
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.10
8
  Donate link: http://david.dw-perspective.org.uk/donate
9
+ License: GPL3
10
  Author URI: http://wordshell.net
11
  */
12
 
13
  //TODO (some of these items mine, some from original Updraft awaiting review):
14
+ //GoogleDrive resume partial upload support (store the current status in a transient after each chunk; use that on resumption)
15
  //Add DropBox support
16
  //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?).
17
  //improve error reporting. s3 and dir backup have decent reporting now, but not sure i know what to do from here
23
  //Rip out the "last backup" bit, and/or put in a display of the last log
24
 
25
  /* More TODO:
 
26
  Use only one entry in WP options database
27
  Encrypt filesystem, if memory allows (and have option for abort if not); split up into multiple zips when needed
28
  // Does not delete old custom directories upon a restore?
30
 
31
  /* Portions copyright 2010 Paul Kehrer
32
  Portions copyright 2011-12 David Anderson
33
+ Other portions copyright as indicated authors in the relevant files
34
+ Particular thanks to Sorin Iclanzan, author of the "Backup" plugin, from which much Google Drive code was taken under the GPLv3+
35
 
36
  This program is free software; you can redistribute it and/or modify
37
  it under the terms of the GNU General Public License as published by
38
+ the Free Software Foundation; either version 3 of the License, or
39
  (at your option) any later version.
40
 
41
  This program is distributed in the hope that it will be useful,
62
 
63
  class UpdraftPlus {
64
 
65
+ var $version = '0.9.10';
66
 
67
  var $dbhandle;
68
  var $errors = array();
70
  var $logfile_name = "";
71
  var $logfile_handle = false;
72
  var $backup_time;
73
+ var $gdocs;
74
+ var $gdocs_access_token;
75
+ var $gdocs_location;
76
+
77
  function __construct() {
78
  // Initialisation actions
79
  # Create admin page
97
  if ( is_admin() && isset( $_GET['page'] ) && $_GET['page'] == 'updraftplus' && isset( $_GET['action'] ) && $_GET['action'] == 'auth' ) {
98
  if ( isset( $_GET['state'] ) ) {
99
  if ( $_GET['state'] == 'token' )
100
+ $this->gdrive_auth_token();
101
  elseif ( $_GET['state'] == 'revoke' )
102
+ $this->gdrive_auth_revoke();
103
  } elseif (isset($_GET['updraftplus_googleauth'])) {
104
+ $this->gdrive_auth_request();
105
  }
106
  }
107
  }
109
  /**
110
  * Acquire single-use authorization code from Google OAuth 2.0
111
  */
112
+ function gdrive_auth_request() {
113
  $params = array(
114
  'response_type' => 'code',
115
  'client_id' => get_option('updraft_googledrive_clientid'),
155
  }
156
  }
157
 
158
+ function googledrive_delete_file( $file, $token) {
159
+ $ids = get_option('updraft_file_ids', array());
160
+ if (!isset($ids[$file])) {
161
+ $this->log("Could not delete: could not find a record of the Google Drive file ID for this file");
162
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  } else {
164
+ $del == $this->gdocs->delete_resource($ids[$file]);
165
+ if (is_wp_error($del)) {
166
+ foreach ($del->get_error_messages() as $msg) {
167
+ $this->log("Deletion failed: $msg");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  }
169
+ } else {
170
+ $this->log("Deletion successful");
171
+ unset($ids[$file]);
172
+ update_option('updraft_file_ids', $ids);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  }
174
  }
 
 
 
 
 
 
 
 
 
175
  return;
176
  }
177
 
178
  /**
179
+ * Get a Google account refresh token using the code received from gdrive_auth_request
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  */
181
+ function gdrive_auth_token() {
182
  if( isset( $_GET['code'] ) ) {
183
  $context = array(
184
  'http' => array(
221
  /**
222
  * Revoke a Google account refresh token
223
  */
224
+ function gdrive_auth_revoke() {
225
  @file_get_contents( 'https://accounts.google.com/o/oauth2/revoke?token=' . get_option('updraft_googledrive_token') );
226
  update_option('updraft_googledrive_token','');
227
  header( 'Location: '.admin_url( 'options-general.php?page=updraftplus&message=' . __( 'Authorization revoked.', 'backup' ) ) );
249
  }
250
 
251
  function backup_resume($resumption_no) {
252
+ @ignore_user_abort(true);
253
  // This is scheduled for 5 minutes after a backup job starts
254
  $bnonce = get_transient('updraftplus_backup_job_nonce');
255
  if (!$bnonce) return;
333
  //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.
334
  function backup($backup_files, $backup_database) {
335
 
336
+ @ignore_user_abort(true);
337
  //generate backup information
338
  $this->backup_time_nonce();
339
  // If we don't finish in 3 hours, then we won't finish
462
  }
463
 
464
  // This should be called whenever a file is successfully uploaded
465
+ function uploaded_file($file, $id = false) {
466
  # We take an MD5 hash because set_transient wants a name of 45 characters or less
467
  $hash = md5($file);
468
  set_transient("updraft_".$hash, "yes", 3600*3);
469
+ if ($id) {
470
+ $ids = get_option('updraft_file_ids', array() );
471
+ $ids[$file] = $id;
472
+ update_option('updraft_file_ids',$ids);
473
+ $this->log("Stored file<->id correlation in database ($file <-> $id)");
474
+ }
475
  }
476
 
477
  function cloud_backup($backup_array) {
556
  $this->log("$backup_datestamp: Delete remote ftp: $remote_path/$file");
557
  @$remote_object->delete($remote_path.$file);
558
  } elseif ($updraft_service == "googledrive") {
559
+ $this->log("$backup_datestamp: Delete remote file from Google Drive: $file");
560
+ $this->googledrive_delete_file($file,$remote_object);
561
  }
562
  }
563
  unset($backup_to_examine['db']);
600
  $this->log("$backup_datestamp: Delete remote ftp: $remote_path/$dofile");
601
  @$remote_object->delete($remote_path.$dofile);
602
  } elseif ($updraft_service == "googledrive") {
603
+ $this->log("$backup_datestamp: Delete remote file from Google Drive: $dofile");
604
+ $this->googledrive_delete_file($dofile,$remote_object);
605
  }
606
  }
607
  }
652
  $this->error("S3 Error: Failed to create bucket $bucket_name. Error was ".$php_errormsg);
653
  }
654
  }
655
+
656
+ // This function taken from wordpress.org/extend/plugins/backup, by Sorin Iclanzan, under the GPLv3 or later at your choice
657
+ function is_gdocs( $thing ) {
658
+ if ( is_object( $thing ) && is_a( $thing, 'UpdraftPlus_GDocs' ) )
659
+ return true;
660
+ return false;
661
+ }
662
+
663
+ // This function modified from wordpress.org/extend/plugins/backup, by Sorin Iclanzan, under the GPLv3 or later at your choice
664
+ function need_gdocs() {
665
+
666
+ if ( ! $this->is_gdocs( $this->gdocs ) ) {
667
+ if ( get_option('updraft_googledrive_token') == "" || get_option('updraft_googledrive_clientid') == "" || get_option('updraft_googledrive_secret') == "" ) {
668
+ $this->log("GoogleDrive: this account is not authorised");
669
+ return new WP_Error( "not_authorized", "Account is not authorized." );
670
  }
671
+
672
+ if ( is_wp_error( $this->gdocs_access_token ) ) return $access_token;
673
+
674
+ $this->gdocs = new UpdraftPlus_GDocs( $this->gdocs_access_token );
675
+ $this->gdocs->set_option( 'chunk_size', $this->options['chunk_size'] );
676
+ $this->gdocs->set_option( 'time_limit', $this->options['time_limit'] );
677
+ $this->gdocs->set_option( 'request_timeout', $this->options['request_timeout'] );
678
+ $this->gdocs->set_option( 'max_resume_attempts', $this->options['backup_attempts'] );
679
+ }
680
+ return true;
681
+ }
682
+
683
+ function googledrive_upload_file( $file, $title, $parent = '') {
684
+
685
+ // Make sure $this->gdocs is a UpdraftPlus_GDocs object, or give an error
686
+ if ( is_wp_error( $e = $this->need_gdocs() ) ) return false;
687
+
688
+ if ( empty( $this->gdocs_location ) ) {
689
+ $this->log("$file: Attempting to upload file to Google Drive.");
690
+ $location = $this->gdocs->prepare_upload(
691
+ $file,
692
+ $title,
693
+ $parent
694
+ );
695
  } else {
696
+ $this->log('$file: Attempting to resume upload.');
697
+ $location = $this->gdocs->resume_upload(
698
+ $file,
699
+ $this->gdocs_location
700
+ );
701
  }
702
+
703
+ if ( is_wp_error( $location ) ) {
704
+ $this->log("GoogleDrive upload: an error occurred");
705
+ foreach ($location->get_error_messages() as $msg) {
706
+ $this->log("Error details: ".$msg);
707
+ }
708
+ // TODO
709
+ //$this->reschedule_backup( $id );
710
+ return false;
711
+ }
712
+
713
+ if (!is_string($location) && true == $location) {
714
+ $this->log("$file: this file is already uploaded");
715
+ return true;
716
+ }
717
+
718
+ if ( is_string( $location ) ) {
719
+ $res = $location;
720
+ $this->log("Uploading file with title ".$title);
721
+ $d = 0;
722
+ // echo '<div id="progress">';
723
+ do {
724
+ $this->gdocs_location = $res;
725
+ $res = $this->gdocs->upload_chunk();
726
+ $p = $this->gdocs->get_upload_percentage();
727
+ if ( $p - $d >= 1 ) {
728
+ $b = intval( $p - $d );
729
+ // echo '<span style="width:' . $b . '%"></span>';
730
+ $d += $b;
731
+ }
732
+ // $this->options['backup_list'][$id]['percentage'] = $p;
733
+ // $this->options['backup_list'][$id]['speed'] = $this->gdocs->get_upload_speed();
734
+ } while ( is_string( $res ) );
735
+ // echo '</div>';
736
+
737
+ if ( is_wp_error( $res ) ) {
738
+ $this->log( "An error occurred during GoogleDrive upload (2)" );
739
+ # TODO
740
+ // $this->reschedule_backup( $id );
741
+ return false;
742
+ }
743
+
744
+ $this->log("The file was successfully uploaded to Google Drive in ".number_format_i18n( $this->gdocs->time_taken(), 3)." seconds at an upload speed of ".size_format( $this->gdocs->get_upload_speed() )."/s.");
745
+
746
+ $this->gdocs_location = null;
747
+ // unset( $this->options['backup_list'][$id]['location'], $this->options['backup_list'][$id]['attempt'] );
748
+ }
749
+
750
+ return $this->gdocs->get_file_id();
751
+ // unset( $this->options['backup_list'][$id]['percentage'], $this->options['backup_list'][$id]['speed'] );
752
+ // $this->update_quota();
753
+ // Google's "user info" service
754
+ // if ( empty( $this->options['user_info'] ) ) $this->set_user_info();
755
+
756
  }
757
+
758
+ // This function just does the formalities, and off-loads the main work to googledrive_upload_file
759
+ function googledrive_backup($backup_array) {
760
+
761
+ require_once(dirname(__FILE__).'/includes/class-gdocs.php');
762
+
763
+ // Do we have an access token?
764
+ if ( !$access_token = $this->access_token( get_option('updraft_googledrive_token'), get_option('updraft_googledrive_clientid'), get_option('updraft_googledrive_secret') )) {
765
+ $this->log('ERROR: Have not yet obtained an access token from Google (has the user authorised?)');
766
+ return new WP_Error( "no_access_token", "Have not yet obtained an access token from Google (has the user authorised?");
767
+ }
768
+
769
+ $this->gdocs_access_token = $access_token;
770
+
771
+ foreach ($backup_array as $file) {
772
+ $file_path = trailingslashit(get_option('updraft_dir')).$file;
773
+ $file_name = basename($file_path);
774
+ $this->log("$file_name: Attempting to upload to Google Drive");
775
+ $timer_start = microtime( true );
776
+ if ( $id = $this->googledrive_upload_file( $file_path, $file_name, get_option('updraft_googledrive_remotepath')) ) {
777
+ $this->log('OK: Archive ' . $file_name . ' uploaded to Google Drive in ' . ( round(microtime( true ) - $timer_start,2) ) . ' seconds (id: '.$id.')' );
778
+ $this->uploaded_file($file, $id);
779
+ } else {
780
+ $this->error("$file_name: Failed to upload to Google Drive" );
781
+ $this->log("ERROR: $file_name: Failed to upload to Google Drive" );
782
+ }
783
+ }
784
+ $this->prune_retained_backups("googledrive",$access_token,get_option('updraft_googledrive_remotepath'));
785
+ }
786
+
787
  function ftp_backup($backup_array) {
788
  if( !class_exists('ftp_wrapper')) {
789
  require_once(dirname(__FILE__).'/includes/ftp.class.php');
1178
  $row_start = $segment * ROWS_PER_SEGMENT;
1179
  $row_inc = ROWS_PER_SEGMENT;
1180
  }
1181
+ do {
1182
  // don't include extra stuff, if so requested
1183
  $excs = array('revisions' => 0, 'spam' => 1); //TODO, FIX THIS
1184
  $where = '';
1249
  }
1250
  }
1251
 
 
 
 
 
 
1252
  function error($error,$severity='') {
1253
+ $this->errors[] = $error;
 
 
 
1254
  return true;
1255
  }
1256
 
1359
  $len = filesize($fullpath);
1360
 
1361
  $filearr = explode('.',$file);
1362
+ // //we've only got zip and gz...for now
1363
  $file_ext = array_pop($filearr);
1364
  if($file_ext == 'zip') {
1365
  header('Content-type: application/zip');
1419
  }
1420
 
1421
  function download_googledrive_backup($file) {
1422
+
1423
+ require_once(dirname(__FILE__).'/includes/class-gdocs.php');
1424
+
1425
+ // Do we have an access token?
1426
+ if ( !$access_token = $this->access_token( get_option('updraft_googledrive_token'), get_option('updraft_googledrive_clientid'), get_option('updraft_googledrive_secret') )) {
1427
+ $this->error('ERROR: Have not yet obtained an access token from Google (has the user authorised?)');
1428
+ return false;
1429
+ }
1430
+
1431
+ $this->gdocs_access_token = $access_token;
1432
+
1433
+ // Make sure $this->gdocs is a UpdraftPlus_GDocs object, or give an error
1434
+ if ( is_wp_error( $e = $this->need_gdocs() ) ) return false;
1435
+
1436
+ $ids = get_option('updraft_file_ids', array());
1437
+ if (!isset($ids[$file])) {
1438
+ $this->error("Google Drive error: $file: could not download: could not find a record of the Google Drive file ID for this file");
1439
+ return;
1440
+ } else {
1441
+ $content_link = $this->gdocs->get_content_link( $ids[$file], $file );
1442
+ if (is_wp_error($content_link)) {
1443
+ $this->error("Could not find $file in order to download it (id: ".$ids[$file].")");
1444
+ foreach ($content_link->get_error_messages() as $msg) {
1445
+ $this->error($msg);
1446
+ }
1447
+ return false;
1448
+ }
1449
+ // Actually download the thing
1450
+ $download_to = trailingslashit(get_option('updraft_dir')).$file;
1451
+ $this->gdocs->download_data($content_link, $download_to);
1452
+
1453
+ if (filesize($download_to) >0) {
1454
+ return true;
1455
+ } else {
1456
+ $this->error("Google Drive error: zero-size file was downloaded");
1457
+ return false;
1458
+ }
1459
+
1460
+ }
1461
+
1462
+ return;
1463
+
1464
  }
1465
 
1466
  function download_s3_backup($file) {
1485
  }
1486
 
1487
  function download_ftp_backup($file) {
1488
+ if( !class_exists('ftp_wrapper')) require_once(dirname(__FILE__).'/includes/ftp.class.php');
1489
+
 
1490
  //handle SSL and errors at some point TODO
1491
  $ftp = new ftp_wrapper(get_option('updraft_server_address'),get_option('updraft_ftp_login'),get_option('updraft_ftp_pass'));
1492
  $ftp->passive = true;
1711
 
1712
  function settings_output() {
1713
 
 
 
 
 
 
1714
  /*
1715
  we use request here because the initial restore is triggered by a POSTed form. we then may need to obtain credentials
1716
  for the WP_Filesystem. to do this WP outputs a form that we can't insert variables into (apparently). So the values are
1723
  echo '<b>Actions:</b> <a href="options-general.php?page=updraftplus&updraft_restore_success=true">Return to Updraft Configuration</a>.';
1724
  return;
1725
  } else {
1726
+ echo '<p>Restore failed...</p><ul>';
1727
+ foreach ($this->errors as $err) {
1728
+ echo "<li>";
1729
+ if (is_string($err)) { echo htmlspecialchars($err); } else {
1730
+ print_r($err);
1731
+ }
1732
+ echo "</li>";
1733
+ }
1734
+ echo '</ul><b>Actions:</b> <a href="options-general.php?page=updraftplus">Return to Updraft Configuration</a>.';
1735
  return;
1736
  }
1737
  //uncomment the below once i figure out how i want the flow of a restoration to work.
1766
  }
1767
 
1768
  if(isset($_POST['action']) && $_POST['action'] == 'updraft_backup') {
1769
+ echo '<div class="updated fade" style="max-width: 800px; font-size:140%; padding:14px; clear:left;"><strong>Schedule backup:</strong> ';
1770
  if (wp_schedule_single_event(time()+5, 'updraft_backup_all') === false) {
1771
+ echo "Failed.";
1772
  } else {
1773
+ echo "OK. Now load a page from your site to make sure the schedule can trigger.";
1774
  }
1775
+ echo '</div>';
1776
  }
1777
  if(isset($_POST['action']) && $_POST['action'] == 'updraft_backup_debug_all') {
1778
  $this->backup(true,true);
1783
 
1784
  ?>
1785
  <div class="wrap">
1786
+ <h1>UpdraftPlus - Backup/Restore</h1>
1787
 
1788
+ <!-- Version: <b><?php echo $this->version; ?></b><br>-->
1789
+ 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> | <a href="http://profiles.wordpress.org/davidanderson/">My other WordPress plugins</a>). Version: <?php echo $this->version; ?>
1790
+ <br>
 
 
1791
  <?php
1792
  if(isset($_GET['updraft_restore_success'])) {
1793
  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>";
1794
  }
1795
+
1796
+ $ws_advert = $this->wordshell_random_advert(1);
1797
+ echo <<<ENDHERE
1798
+ <div class="updated fade" style="max-width: 800px; font-size:140%; padding:14px; clear:left;">${ws_advert}</div>
1799
+ ENDHERE;
1800
+
1801
+
1802
  if($deleted_old_dirs) {
1803
+ echo '<div style="color:blue">Old directories successfully deleted.</div>';
1804
  }
1805
  if(!$this->memory_check(96)) {?>
1806
  <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>
1826
  }
1827
  }
1828
  ?>
1829
+
1830
+ <h2 style="clear:left;">Existing Schedule And Backups</h2>
1831
+ <table class="form-table" style="float:left; clear: both; width:475px">
1832
  <tr>
1833
  <?php
1834
  $next_scheduled_backup = wp_next_scheduled('updraft_backup');
1863
  $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>';
1864
  }
1865
  ?>
1866
+
1867
+ <th>The Time Now:</th>
1868
  <td style="color:blue"><?php echo $current_time?></td>
1869
  </tr>
1870
  <tr>
1880
  <td style="color:<?php echo $last_backup_color ?>"><?php echo $last_backup?></td>
1881
  </tr>
1882
  </table>
1883
+ <div style="float:left; width:200px; padding-top: 100px;">
1884
  <form method="post" action="">
1885
  <input type="hidden" name="action" value="updraft_backup" />
1886
+ <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>
1887
  </form>
1888
  <div style="position:relative">
1889
  <div style="position:absolute;top:0;left:0">
1984
  </table>
1985
  <form method="post" action="options.php">
1986
  <?php settings_fields('updraft-options-group'); ?>
1987
+ <h2>Configure Backup Contents And Schedule</h2>
1988
+ <table class="form-table" style="width:850px;">
 
 
 
 
 
 
1989
  <tr>
1990
  <th>File Backup Intervals:</th>
1991
  <td><select name="updraft_interval">
2026
  <tr>
2027
  <th>Include in Files Backup:</th>
2028
  <td>
2029
+ <input type="checkbox" name="updraft_include_plugins" value="1" <?php echo $include_plugins; ?> /> Plugins<br>
2030
+ <input type="checkbox" name="updraft_include_themes" value="1" <?php echo $include_themes; ?> /> Themes<br>
2031
+ <input type="checkbox" name="updraft_include_uploads" value="1" <?php echo $include_uploads; ?> /> Uploads<br>
2032
+ <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>
2033
+ 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>
2034
  </td>
2035
  </tr>
2036
  <tr>
2041
  ?>
2042
  <td><input type="text" name="updraft_retain" value="<?php echo $retain ?>" style="width:50px" /></td>
2043
  </tr>
2044
+ <tr class="email" <?php echo $email_display?>>
2045
+ <th>Email:</th>
2046
+ <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>
2047
+ </tr>
2048
+ <tr class="deletelocal s3 ftp email" <?php echo $display_delete_local?>>
2049
+ <th>Delete local backup:</th>
2050
+ <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 (below), otherwise you will have no backup remaining).</td>
2051
+ </tr>
2052
+
2053
  <tr class="backup-retain-description">
2054
  <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>
2055
  </tr>
2063
  <tr class="backup-crypt-description">
2064
  <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>
2065
  </tr>
2066
+ </table>
2067
+
2068
+ <h2>Copying Your Backup To Remote Storage</h2>
2069
 
2070
+ <table class="form-table" style="width:850px;">
2071
  <tr>
2072
  <th>Remote backup:</th>
2073
  <td><select name="updraft_service" id="updraft-service">
2114
  ?>
2115
  <option value="none" <?php echo $none?>>None</option>
2116
  <option value="s3" <?php echo $s3?>>Amazon S3</option>
2117
+ <option value="googledrive" <?php echo $googledrive?>>Google Drive</option>
2118
  <option value="ftp" <?php echo $ftp?>>FTP</option>
2119
  <option value="email" <?php echo $email?>>E-mail</option>
2120
  </select></td>
2121
  </tr>
2122
  <tr class="backup-service-description">
2123
+ <td></td><td>Choose your backup method. If choosing &quot;E-Mail&quot;, then be aware that mail servers tend to have size limits; typically around 10-20Mb; backups larger than any limits will not arrive.</td>
2124
 
2125
  </tr>
2126
 
2153
  </tr>
2154
  <tr class="googledrive" <?php echo $googledrive_display?>>
2155
  <th>Google Drive Folder ID:</th>
2156
+ <td><input type="text" style="width:332px" name="updraft_googledrive_remotepath" value="<?php echo get_option('updraft_googledrive_remotepath'); ?>" /> <em>(To get a folder's ID navigate to that folder in Google Drive in your web browser and copy the ID from your browser's address bar. It is the part that comes after <kbd>#folders/.</kbd> Leave empty to use your root folder)</em></td>
2157
  </tr>
2158
  <tr class="googledrive" <?php echo $googledrive_display?>>
2159
  <th>Authenticate with Google:</th>
2165
  }
2166
  ?>
2167
  </p>
 
 
2168
  </td>
2169
  </tr>
2170
  <tr class="googledrive" <?php echo $googledrive_display?>>
2171
  <th></th>
2172
+ <td>
2173
+ 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.
2174
+
2175
  <?php
2176
+ if (!class_exists('SimpleXMLElement')) { echo " <b>WARNING:</b> You do not have the SimpleXMLElement installed. Google Drive backups will <b>not</b> work until you do."; }
2177
  ?>
2178
+
2179
  </td>
2180
  </tr>
2181
 
2198
  <tr class="ftp-description" style="display:none">
2199
  <td colspan="2">An FTP remote path will look like '/home/backup/some/folder'</td>
2200
  </tr>
2201
+ </table>
2202
+ <table class="form-table" style="width:850px;">
2203
+ <tr><td colspan="2"><h2>Advanced / Debugging Settings</h2></td></tr>
2204
+ <tr>
2205
+ <th>Backup Directory:</th>
2206
+ <td><input type="text" name="updraft_dir" style="width:525px" value="<?php echo $updraft_dir ?>" /></td>
2207
  </tr>
2208
+ <tr>
2209
+ <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>
 
2210
  </tr>
2211
  <tr>
2212
  <th>Debug mode:</th>
2213
+ <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>
2214
  </tr>
2215
  <tr>
2216
  <td>
2223
  <?php
2224
  if(get_option('updraft_debug_mode')) {
2225
  ?>
2226
+ <div style="padding-top: 40px;">
2227
+ <hr>
2228
  <h3>Debug Information</h3>
2229
  <?php
2230
  $peak_memory_usage = memory_get_peak_usage(true)/1024/1024;
2243
  </form>
2244
  </div>
2245
  <?php } ?>
2246
+
2247
+ <p><em>UpdraftPlus is based on the original 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> )</em></p>
2248
+
2249
+
2250
  <script type="text/javascript">
2251
  jQuery(document).ready(function() {
2252
  jQuery('#updraft-service').change(function() {
2253
  switch(jQuery(this).val()) {
2254
  case 'none':
2255
+ jQuery('.deletelocal,.s3,.ftp,.googledrive,.s3-description,.ftp-description').fadeOut()
2256
+ jQuery('.email,.email-complete').fadeIn()
2257
  break;
2258
  case 's3':
2259
+ jQuery('.ftp,.ftp-description,.googledrive').fadeOut()
2260
+ jQuery('.s3,.deletelocal,.email,.email-complete').fadeIn()
2261
  break;
2262
  case 'googledrive':
2263
+ jQuery('.ftp,.ftp-description,.s3').fadeOut()
2264
+ jQuery('.googledrive,.deletelocal,.googledrive,.email,.email-complete').fadeIn()
2265
  break;
2266
  case 'ftp':
2267
+ jQuery('.googledrive,.s3,.s3-description').fadeOut()
2268
+ jQuery('.ftp,.deletelocal,.email,.email-complete').fadeIn()
2269
  break;
2270
  case 'email':
2271
+ jQuery('.s3,.ftp,.s3-description,.googledrive,.ftp-description,.email-complete').fadeOut()
2272
+ jQuery('.email,.deletelocal').fadeIn()
2273
  break;
2274
  }
2275
  })