PluginProbe
InfiniteWP Client / trunk
InfiniteWP Client vtrunk
1.13.10 1.13.7 trunk 0.1.4 0.1.5 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.1.0 1.1.1 1.1.10 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 1.1.9 1.11.0 1.11.1 1.12.1 1.12.3 All 92 releases
iwp-client / backup / s3.php

s3.php in InfiniteWP Client trunk, at backup/s3.php

900 lines 33.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3
4 if ( ! defined('ABSPATH') )
5 die();
6
7 class IWP_MMB_S3Exception extends Exception {
8 public function __construct($message, $file, $line, $code = 0) {
9 parent::__construct($message, $code);
10 $this->file = $file;
11 $this->line = $line;
12 }
13 }
14
15 if (!class_exists('IWP_MMB_UploadModule')) require_once($GLOBALS['iwp_mmb_plugin_dir'].'/backup/backup.upload.php');
16
17 class IWP_MMB_UploadModule_s3 extends IWP_MMB_UploadModule {
18
19 private $s3_object;
20
21 private $got_with;
22
23 protected $quota_used = null;
24
25 protected $s3_exception;
26
27 // protected $download_chunk_size = 1024*(1*1024);
28 protected $download_chunk_size = 10485760;
29
30 /**
31 * Retrieve specific options for this remote storage module
32 *
33 * @return Array - an array of options
34 */
35 protected function get_config() {
36 $opts = $this->get_options();
37 $opts['whoweare'] = 'S3';
38 $opts['whoweare_long'] = 'Amazon S3';
39 $opts['key'] = 's3';
40 return $opts;
41 }
42
43 /**
44 * This method overrides the parent method and lists the supported features of this remote storage option.
45 *
46 * @return Array - an array of supported features (any features not mentioned are asuumed to not be supported)
47 */
48 public function get_supported_features() {
49 // This options format is handled via only accessing options via $this->get_options()
50 return array('multi_options');
51 }
52
53 /**
54 * Retrieve default options for this remote storage module.
55 *
56 * @return Array - an array of options
57 */
58 public function get_default_options() {
59 return array(
60 'accesskey' => '',
61 'secretkey' => '',
62 'path' => '',
63 'rrs' => '',
64 'server_side_encryption' => '',
65 );
66 }
67
68 protected function indicate_s3_class() {
69 // N.B. : The classes must have different names, as if multiple remote storage options are chosen, then we could theoretically need both (if both Amazon and a compatible-S3 provider are used)
70 // Conditional logic, for new AWS SDK (N.B. 3.x branch requires PHP 5.5, so we're on 2.x - requires 5.3.3)
71
72 $opts = $this->get_config();
73 // IWP_MMB_S3 is used when not accessing Amazon Web Services
74 $class_to_use = 'IWP_MMB_S3';
75 if (version_compare(PHP_VERSION, '5.3.3', '>=') && !empty($opts['key']) && ('s3' == $opts['key']) && (!defined('IWP_S3_OLDLIB') || !IWP_S3_OLDLIB)) {
76 $class_to_use = 'IWP_MMB_S3_Compat';
77 }
78
79 if ('IWP_MMB_S3_Compat' == $class_to_use) {
80 if (!class_exists($class_to_use)) include_once($GLOBALS['iwp_mmb_plugin_dir'].'/lib/S3compat.php');
81 } else {
82 if (!class_exists($class_to_use)) include_once($GLOBALS['iwp_mmb_plugin_dir'].'/lib/S3.php');
83 }
84 return $class_to_use;
85
86 }
87
88 /**
89 * Get an S3 object, after setting our options
90 *
91 * @param string $key S3 Key
92 * @param string $secret S3 secret
93 * @param boolean $useservercerts User server certificates
94 * @param boolean $disableverify Check if disableverify is enabled
95 * @param boolean $nossl Check if there is SSL or not
96 * @param string $endpoint S3 endpoint
97 * @param boolean $sse A flag to use server side encryption
98 * @return array
99 */
100 public function getS3($key, $secret, $useservercerts, $disableverify, $nossl, $endpoint = null, $sse = false) {
101
102 if (!empty($this->s3_object) && !is_wp_error($this->s3_object)) return $this->s3_object;
103
104 if (is_string($key)) $key = trim($key);
105 if (is_string($secret)) $secret = trim($secret);
106
107 // Saved in case the object needs recreating for the corner-case where there is no permission to look up the bucket location
108 $this->got_with = array(
109 'key' => $key,
110 'secret' => $secret,
111 'useservercerts' => $useservercerts,
112 'disableverify' => $disableverify,
113 'nossl' => $nossl,
114 'server_side_encryption' => $sse
115 );
116
117 if (is_wp_error($key)) return $key;
118
119 if ('' == $key || '' == $secret) {
120 return new WP_Error('no_settings', __('No settings were found - please go to the Settings tab and check your settings', 'InfiniteWP'));
121 }
122
123 global $iwp_backup_core;
124
125 $use_s3_class = $this->indicate_s3_class();
126
127 if (!class_exists('WP_HTTP_Proxy')) include_once(ABSPATH.WPINC.'/class-http.php');
128 $proxy = new WP_HTTP_Proxy();
129
130 $use_ssl = true;
131 $ssl_ca = true;
132 if (!$nossl) {
133 $curl_version = (function_exists('curl_version')) ? curl_version() : array('features' => null);
134 $curl_ssl_supported = ($curl_version['features'] & defined('CURL_VERSION_SSL') && CURL_VERSION_SSL);
135 if ($curl_ssl_supported) {
136 if ($disableverify) {
137 $ssl_ca = false;
138 $iwp_backup_core->log("S3: Disabling verification of SSL certificates");
139 } else {
140 if ($useservercerts) {
141 $iwp_backup_core->log("S3: Using the server's SSL certificates");
142 $ssl_ca = 'system';
143 } else {
144 $ssl_ca = file_exists($GLOBALS['iwp_mmb_plugin_dir'].'/lib/cacert.pem') ? $GLOBALS['iwp_mmb_plugin_dir'].'/lib/cacert.pem' : true;
145 }
146 }
147 } else {
148 $use_ssl = false;
149 $iwp_backup_core->log("S3: Curl/SSL is not available. Communications will not be encrypted.");
150 }
151 } else {
152 $use_ssl = false;
153 $iwp_backup_core->log("SSL was disabled via the user's preference. Communications will not be encrypted.");
154 }
155
156 try {
157 $s3 = new $use_s3_class($key, $secret, $use_ssl, $ssl_ca, $endpoint);
158 } catch (Exception $e) {
159
160 // Catch a specific PHP engine bug - see HS#6364
161 if ('IWP_MMB_S3_Compat' == $use_s3_class && is_a($e, 'InvalidArgumentException') && false !== strpos('Invalid signature type: s3', $e->getMessage())) {
162 include_once($GLOBALS['iwp_mmb_plugin_dir'].'/lib/S3.php');
163 $use_s3_class = 'IWP_MMB_S3';
164 $try_again = true;
165 } else {
166 $iwp_backup_core->log(sprintf(__('%s Error: Failed to initialise', 'iwp_backup_core'), 'S3').": ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')');
167 $iwp_backup_core->log(sprintf(__('%s Error: Failed to initialise', 'InfiniteWP'), $key), 'S3');
168 return new WP_Error('s3_init_failed', sprintf(__('%s Error: Failed to initialise', 'InfiniteWP'), 'S3').": ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')');
169 }
170 }
171
172 if (!empty($try_again)) {
173 try {
174 $s3 = new $use_s3_class($key, $secret, $use_ssl, $ssl_ca, $endpoint);
175 } catch (Exception $e) {
176 $iwp_backup_core->log(sprintf(__('%s Error: Failed to initialise', 'InfiniteWP'), 'S3').": ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')');
177 $iwp_backup_core->log(sprintf(__('%s Error: Failed to initialise', 'InfiniteWP'), $key), 'S3');
178 return new WP_Error('s3_init_failed', sprintf(__('%s Error: Failed to initialise', 'InfiniteWP'), 'S3').": ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')');
179 }
180 $iwp_backup_core->log("S3: Hit a PHP engine bug - had to switch to the older S3 library (which is incompatible with signatureV4, which may cause problems later on if using a region that requires it)");
181 }
182
183 if ($proxy->is_enabled()) {
184 // WP_HTTP_Proxy returns empty strings where we want nulls
185 $user = $proxy->username();
186 if (empty($user)) {
187 $user = null;
188 $pass = null;
189 } else {
190 $pass = $proxy->password();
191 if (empty($pass)) $pass = null;
192 }
193 $port = (int) $proxy->port();
194 if (empty($port)) $port = 8080;
195 $s3->setProxy($proxy->host(), $user, $pass, CURLPROXY_HTTP, $port);
196 }
197
198 if (method_exists($s3, 'setServerSideEncryption') ) $s3->setServerSideEncryption('AES256');
199
200 $this->s3_object = $s3;
201
202 return $this->s3_object;
203 }
204
205 protected function set_region($obj, $region, $bucket_name = '') {
206 global $iwp_backup_core;
207 switch ($region) {
208 case 'EU':
209 case 'eu-west-1':
210 $endpoint = 's3-eu-west-1.amazonaws.com';
211 break;
212 case 'us-east-1':
213 $endpoint = 's3.amazonaws.com';
214 break;
215 case 'us-west-1':
216 case 'us-east-2':
217 case 'us-west-2':
218 case 'eu-west-2':
219 case 'ap-southeast-1':
220 case 'ap-southeast-2':
221 case 'ap-northeast-1':
222 case 'ap-northeast-2':
223 case 'sa-east-1':
224 case 'eu-west-3':
225 case 'ca-central-1':
226 case 'us-gov-west-1':
227 case 'eu-central-1':
228 $endpoint = 's3-'.$region.'.amazonaws.com';
229 break;
230 case 'ap-south-1':
231 case 'cn-north-1':
232 $endpoint = 's3.'.$region.'.amazonaws.com.cn';
233 break;
234 default:
235 break;
236 }
237
238 if (isset($endpoint)) {
239 if (is_a($obj, 'IWP_MMB_S3_Compat')) {
240 $iwp_backup_core->log("Set region: $region");
241 $obj->setRegion($region);
242 return;
243 }
244
245 $iwp_backup_core->log("Set endpoint: $endpoint");
246
247 return $obj->setEndpoint($endpoint);
248 }
249 }
250
251 /**
252 * Perform the upload of backup archives
253 *
254 * @param Array $backup_array - a list of file names (basenames) (within UD's directory) to be uploaded
255 *
256 * @return Mixed - return (boolean)false ot indicate failure, or anything else to have it passed back at the delete stage (most useful for a storage object).
257 */
258 public function backup($backup_array) {
259
260 global $iwp_backup_core;
261
262 $config = $this->get_config();
263
264 if (empty($config['accesskey']) && !empty($config['error_message'])) {
265 $err = new WP_Error('no_settings', $config['error_message']);
266 return $iwp_backup_core->log_wp_error($err, false, true);
267 }
268
269 $whoweare = $config['whoweare'];
270 $whoweare_key = $config['key'];
271 $whoweare_keys = substr($whoweare_key, 0, 3);
272 $sse = empty($config['server_side_encryption']) ? false : true;
273
274 $s3 = $this->getS3(
275 $config['accesskey'],
276 $config['secretkey'],
277 IWP_MMB_Backup_Options::get_iwp_backup_option('IWP_ssl_useservercerts'),
278 IWP_MMB_Backup_Options::get_iwp_backup_option('IWP_ssl_disableverify'),
279 IWP_MMB_Backup_Options::get_iwp_backup_option('IWP_ssl_nossl'),
280 null,
281 $sse
282 );
283
284 if (is_wp_error($s3)) return $iwp_backup_core->log_wp_error($s3, false, true);
285
286 if (is_a($s3, 'IWP_MMB_S3_Compat') && !class_exists('XMLWriter')) {
287 $iwp_backup_core->log('The required XMLWriter PHP module is not installed');
288 $iwp_backup_core->log(sprintf(__('The required %s PHP module is not installed - ask your web hosting company to enable it', 'InfiniteWP'), 'XMLWriter'), 'error');
289 return false;
290 }
291
292 $bucket_name = untrailingslashit($config['path']);
293 if (!empty($config['as3_site_folder'])) {
294 $site_name = iwp_getSiteName();
295 $bucket_name.= '/'.untrailingslashit($site_name);
296 }
297 $bucket_path = "";
298 $orig_bucket_name = $bucket_name;
299
300 if (preg_match("#^([^/]+)/(.*)$#", $bucket_name, $bmatches)) {
301 $bucket_name = $bmatches[1];
302 $bucket_path = $bmatches[2]."/";
303 }
304
305 list($s3, $bucket_exists, $region) = $this->get_bucket_access($s3, $config, $bucket_name, $bucket_path);
306
307 // See if we can detect the region (which implies the bucket exists and is ours), or if not create it
308 if ($bucket_exists) {
309
310 $iwp_backup_dir = trailingslashit($iwp_backup_core->backups_dir_location());
311
312 foreach ($backup_array as $key => $file) {
313
314 // We upload in 5MB chunks to allow more efficient resuming and hence uploading of larger files
315 // N.B.: 5MB is Amazon's minimum. So don't go lower or you'll break it.
316 $fullpath = $iwp_backup_dir.$file;
317 $orig_file_size = filesize($fullpath);
318
319 if (!file_exists($fullpath)) {
320 $iwp_backup_core->log("File not found: $file: $whoweare: ");
321 $iwp_backup_core->log("$file: ".sprintf(__('Error: %s', 'InfiniteWP'), __('File not found', 'InfiniteWP')), 'error');
322 continue;
323 }
324
325 if (isset($config['quota']) && method_exists($this, 's3_get_quota_info')) {
326 $quota_used = $this->s3_get_quota_info('numeric', $config['quota']);
327 if (false === $quota_used) {
328 $iwp_backup_core->log("Quota usage: count failed");
329 } else {
330 $this->quota_used = $quota_used;
331 if ($config['quota'] - $this->quota_used < $orig_file_size) {
332 if (method_exists($this, 's3_out_of_quota')) call_user_func(array($this, 's3_out_of_quota'), $config['quota'], $this->quota_used, $orig_file_size);
333 continue;
334 } else {
335 // We don't need to log this always - the s3_out_of_quota method will do its own logging
336 $iwp_backup_core->log("$whoweare: Quota is available: used=$quota_used (".round($quota_used/1048576, 1)." MB), total=".$config['quota']." (".round($config['quota']/1048576, 1)." MB), needed=$orig_file_size (".round($orig_file_size/1048576, 1)." MB)");
337 }
338 }
339 }
340
341 $chunks = floor($orig_file_size / 5242880);
342 // There will be a remnant unless the file size was exactly on a 5MB boundary
343 if ($orig_file_size % 5242880 > 0) $chunks++;
344 $hash = md5($file);
345
346 $iwp_backup_core->log("$whoweare upload ($region): $file (chunks: $chunks) -> $whoweare_key://$bucket_name/$bucket_path$file");
347
348 $filepath = $bucket_path.$file;
349
350 // This is extra code for the 1-chunk case, but less overhead (no bothering with job data)
351 if ($chunks < 2) {
352 $s3->setExceptions(true);
353 try {
354 if (!$s3->putObjectFile($fullpath, $bucket_name, $filepath, 'private', array(), array(), apply_filters('IWP_'.$whoweare_key.'_storageclass', 'STANDARD', $s3, $config))) {
355 $iwp_backup_core->log("$whoweare regular upload: failed ($fullpath)");
356 $iwp_backup_core->log("$file: ".sprintf(__('%s Error: Failed to upload', 'InfiniteWP'), $whoweare), 'error');
357 } else {
358 $this->quota_used += $orig_file_size;
359 if (method_exists($this, 's3_record_quota_info')) $this->s3_record_quota_info($this->quota_used, $config['quota']);
360 $extra_log = '';
361 if (method_exists($this, 's3_get_quota_info')) {
362 $extra_log = ', quota used now: '.round($this->quota_used / 1048576, 1).' MB';
363 }
364 $iwp_backup_core->log("$whoweare regular upload: success$extra_log");
365 $iwp_backup_core->uploaded_file($file);
366 }
367 } catch (Exception $e) {
368 $iwp_backup_core->log("$file: ".sprintf(__('%s Error: Failed to upload', 'InfiniteWP'), $whoweare).": ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile());
369 $iwp_backup_core->log("$file: ".sprintf(__('%s Error: Failed to upload', 'InfiniteWP'), $whoweare), 'error');
370 }
371 $s3->setExceptions(false);
372 } else {
373
374 // Retrieve the upload ID
375 $upload_id = $this->jobdata_get($hash.'_uid', null, "upd_".$whoweare_keys."_".$hash."_uid");
376 if (empty($upload_id)) {
377 $s3->setExceptions(true);
378 try {
379 $upload_id = $s3->initiateMultipartUpload($bucket_name, $filepath, 'private', array(), array(), apply_filters('IWP_'.$whoweare_key.'_storageclass', 'STANDARD', $s3, $config));
380 } catch (Exception $e) {
381 $iwp_backup_core->log("$whoweare error whilst trying initiateMultipartUpload: ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')');
382 $upload_id = false;
383 }
384 $s3->setExceptions(false);
385
386 if (empty($upload_id)) {
387 $iwp_backup_core->log("$whoweare upload: failed: could not get uploadId for multipart upload ($filepath)");
388 $iwp_backup_core->log(sprintf(__("%s upload: getting uploadID for multipart upload failed - see log file for more details", 'InfiniteWP'), $whoweare), 'error');
389 continue;
390 } else {
391 $iwp_backup_core->log("$whoweare chunked upload: got multipart ID: $upload_id");
392 $this->jobdata_set($hash.'_uid', $upload_id, "upd_".$whoweare_keys."_".$hash."_uid");
393 }
394 } else {
395 $iwp_backup_core->log("$whoweare chunked upload: retrieved previously obtained multipart ID: $upload_id");
396 }
397
398 $successes = 0;
399 $etags = array();
400 for ($i = 1; $i <= $chunks; $i++) {
401 $etag = $this->jobdata_get($hash.'_etag_'.$i, null, "ud_".$whoweare_keys."_".$hash."_e$i");
402 if (strlen($etag) > 0) {
403 $iwp_backup_core->log("$whoweare chunk $i: was already completed (etag: $etag)");
404 $successes++;
405 array_push($etags, $etag);
406 } else {
407 // Sanity check: we've seen a case where an overlap was truncating the file from underneath us
408 if (filesize($fullpath) < $orig_file_size) {
409 $iwp_backup_core->log("$whoweare error: $key: chunk $i: file was truncated underneath us (orig_size=$orig_file_size, now_size=".filesize($fullpath).")");
410 $iwp_backup_core->log(sprintf(__('%s error: file %s was shortened unexpectedly', 'InfiniteWP'), $whoweare, $fullpath), 'error');
411 }
412 $etag = $s3->uploadPart($bucket_name, $filepath, $upload_id, $fullpath, $i);
413 if (false !== $etag && is_string($etag)) {
414 $iwp_backup_core->record_uploaded_chunk(round(100*$i/$chunks, 1), "$i, $etag", $fullpath);
415 array_push($etags, $etag);
416 $this->jobdata_set($hash.'_etag_'.$i, $etag, "ud_".$whoweare_keys."_".$hash."_e$i");
417 $successes++;
418 } else {
419 $iwp_backup_core->log("$whoweare chunk $i: upload failed");
420 $iwp_backup_core->log(sprintf(__("%s chunk %s: upload failed", 'InfiniteWP'), $whoweare, $i), 'error');
421 }
422 }
423 }
424 if ($successes >= $chunks) {
425 $iwp_backup_core->log("$whoweare upload: all chunks uploaded; will now instruct $whoweare to re-assemble");
426
427 $s3->setExceptions(true);
428 try {
429 if ($s3->completeMultipartUpload($bucket_name, $filepath, $upload_id, $etags)) {
430 $iwp_backup_core->log("$whoweare upload ($key): re-assembly succeeded");
431 $iwp_backup_core->uploaded_file($file);
432 $this->quota_used += $orig_file_size;
433 if (method_exists($this, 's3_record_quota_info')) $this->s3_record_quota_info($this->quota_used, $config['quota']);
434 } else {
435 $iwp_backup_core->log("$whoweare upload ($key): re-assembly failed ($file)");
436 $iwp_backup_core->log(sprintf(__('%s upload (%s): re-assembly failed (see log for more details)', 'InfiniteWP'), $whoweare, $key), 'error');
437 }
438 } catch (Exception $e) {
439 $iwp_backup_core->log("$whoweare re-assembly error ($key): ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')');
440 $iwp_backup_core->log($e->getMessage().": ".sprintf(__('%s re-assembly error (%s): (see log file for more)', 'InfiniteWP'), $whoweare, $e->getMessage()), 'error');
441 }
442 // Remember to unset, as the deletion code later reuses the object
443 $s3->setExceptions(false);
444 } else {
445 $iwp_backup_core->log("$whoweare upload: upload was not completely successful on this run");
446 }
447 }
448 }
449
450 // Allows counting of the final quota accurately
451 if (method_exists($this, 's3_prune_retained_backups_finished')) {
452 add_action('IWP_prune_retained_backups_finished', array($this, 's3_prune_retained_backups_finished'));
453 }
454
455 return array('s3_object' => $s3, 's3_orig_bucket_name' => $orig_bucket_name);
456 } else {
457
458 $extra_text = empty($this->s3_exception) ? '' : ' '.$this->s3_exception->getMessage().' (line: '.$this->s3_exception->getLine().', file: '.$this->s3_exception->getFile().')';
459 $extra_text_short = empty($this->s3_exception) ? '' : ' '.$this->s3_exception->getMessage();
460
461 $iwp_backup_core->log("$whoweare Error: Failed to access bucket $bucket_name.".$extra_text);
462 $iwp_backup_core->log(sprintf(__('%s Error: Failed to access bucket %s. Check your permissions and credentials.', 'InfiniteWP'), $whoweare, $bucket_name).$extra_text_short, 'error');
463 }
464 }
465
466 public function listfiles($match = 'backup_') {
467 $config = $this->get_config();
468 return $this->listfiles_with_path($config['path'], $match);
469 }
470
471 protected function possibly_wait_for_bucket_or_user($config, $s3) {
472 if (!empty($config['is_new_bucket'])) {
473 if (method_exists($s3, 'waitForBucket')) {
474 $s3->setExceptions(true);
475 try {
476 $s3->waitForBucket($bucket_name);
477 } catch (Exception $e) {
478 // This seems to often happen - we get a 403 on a newly created user/bucket pair, even though the bucket was already waited for by the creator
479 // We could just sleep() - a sleep(5) seems to do it. However, given that it's a new bucket, that's unnecessary.
480 $s3->setExceptions(false);
481 return array();
482 }
483 $s3->setExceptions(false);
484 } else {
485 sleep(4);
486 }
487 } elseif (!empty($config['is_new_user'])) {
488 // A crude waiter, because the AWS toolkit does not have one for IAM propagation - basically, loop around a few times whilst the access attempt still fails
489 $attempt_flag = 0;
490 while ($attempt_flag < 5) {
491
492 $attempt_flag++;
493 if (@$s3->getBucketLocation($bucket_name)) {
494 $attempt_flag = 100;
495 } else {
496
497 sleep($attempt_flag*1.5 + 1);
498
499 // Get the bucket object again... because, for some reason, the AWS PHP SDK (at least on the current version we're using, March 2016) calculates an incorrect signature on subsequent attempts
500 $this->s3_object = null;
501 $s3 = $this->getS3(
502 $config['accesskey'],
503 $config['secretkey'],
504 IWP_MMB_Backup_Options::get_iwp_backup_option('IWP_ssl_useservercerts'),
505 IWP_MMB_Backup_Options::get_iwp_backup_option('IWP_ssl_disableverify'),
506 IWP_MMB_Backup_Options::get_iwp_backup_option('IWP_ssl_nossl'),
507 null
508 );
509
510 if (is_wp_error($s3)) return $s3;
511 if (!is_a($s3, 'IWP_MMB_S3') && !is_a($s3, 'IWP_MMB_S3_Compat')) return new WP_Error('no_s3object', 'Failed to gain access to '.$config['whoweare']);
512
513 }
514 }
515 }
516
517 return $s3;
518 }
519
520 /**
521 * The purpose of splitting this into a separate method, is to also allow listing with a different path
522 *
523 * @param string $path Path to check
524 * @param string $match THe match for idetifying the bucket name
525 * @param boolean $include_subfolders Check if list file need to include sub folders
526 * @return array
527 */
528 public function listfiles_with_path($path, $match = 'backup_', $include_subfolders = false) {
529
530 $bucket_name = untrailingslashit($path);
531 $bucket_path = '';
532 $config = $this->get_config();
533 if (!empty($config['as3_site_folder'])) {
534 $site_name = iwp_getSiteName();
535 $bucket_name.= '/'.untrailingslashit($site_name);
536 }
537 if (preg_match("#^([^/]+)/(.*)$#", $bucket_name, $bmatches)) {
538 $bucket_name = $bmatches[1];
539 $bucket_path = trailingslashit($bmatches[2]);
540 }
541
542
543 global $iwp_backup_core;
544
545 $whoweare = $config['whoweare'];
546 $whoweare_key = $config['key'];
547 $sse = empty($config['server_side_encryption']) ? false : true;
548
549 $s3 = $this->getS3(
550 $config['accesskey'],
551 $config['secretkey'],
552 IWP_MMB_Backup_Options::get_iwp_backup_option('IWP_ssl_useservercerts'),
553 IWP_MMB_Backup_Options::get_iwp_backup_option('IWP_ssl_disableverify'),
554 IWP_MMB_Backup_Options::get_iwp_backup_option('IWP_ssl_nossl'),
555 null,
556 $sse
557 );
558
559 if (is_wp_error($s3)) return $s3;
560 if (!is_a($s3, 'IWP_MMB_S3') && !is_a($s3, 'IWP_MMB_S3_Compat')) return new WP_Error('no_s3object', 'Failed to gain access to '.$config['whoweare']);
561
562 $s3 = $this->possibly_wait_for_bucket_or_user($config, $s3);
563 if (!is_a($s3, 'IWP_MMB_S3') && !is_a($s3, 'IWP_MMB_S3_Compat')) return $s3;
564
565 list($s3, $bucket_exists, $region) = $this->get_bucket_access($s3, $config, $bucket_name, $bucket_path);
566
567 $bucket = $s3->getBucket($bucket_name, $bucket_path.$match);
568
569 if (!is_array($bucket)) return array();
570
571 $results = array();
572
573 foreach ($bucket as $key => $object) {
574 if (!is_array($object) || empty($object['name'])) continue;
575 if (isset($object['size']) && 0 == $object['size']) continue;
576
577 if ($bucket_path) {
578 if (0 !== strpos($object['name'], $bucket_path)) continue;
579 $object['name'] = substr($object['name'], strlen($bucket_path));
580 } else {
581 if (!$include_subfolders && false !== strpos($object['name'], '/')) continue;
582 }
583
584 $result = array('name' => $object['name']);
585 if (isset($object['size'])) $result['size'] = $object['size'];
586 unset($bucket[$key]);
587 $results[] = $result;
588 }
589
590 return $results;
591
592 }
593
594 public function delete($files, $s3arr = false, $sizeinfo = array()) {
595
596 global $iwp_backup_core;
597 if (is_string($files)) $files = array($files);
598
599 $config = $this->get_config();
600 $sse = (empty($config['server_side_encryption'])) ? false : true;
601 $whoweare = $config['whoweare'];
602
603 if ($s3arr) {
604 $s3 = $s3arr['s3_object'];
605 $orig_bucket_name = $s3arr['s3_orig_bucket_name'];
606 } else {
607
608 $s3 = $this->getS3(
609 $config['accesskey'],
610 $config['secretkey'],
611 IWP_MMB_Backup_Options::get_iwp_backup_option('IWP_ssl_useservercerts'),
612 IWP_MMB_Backup_Options::get_iwp_backup_option('IWP_ssl_disableverify'),
613 IWP_MMB_Backup_Options::get_iwp_backup_option('IWP_ssl_nossl'),
614 null,
615 $sse
616 );
617
618 if (is_wp_error($s3)) return $iwp_backup_core->log_wp_error($s3, false, false);
619
620 $bucket_name = untrailingslashit($config['path']);
621 if (!empty($config['as3_site_folder'])) {
622 $site_name = iwp_getSiteName();
623 $bucket_name.= '/'.untrailingslashit($site_name);
624 }
625 $orig_bucket_name = $bucket_name;
626 if (preg_match("#^([^/]+)/(.*)$#", $bucket_name, $bmatches)) {
627 $bucket_name = $bmatches[1];
628 $bucket_path = $bmatches[2]."/";
629 } else {
630 $bucket_path = '';
631 }
632
633 list($s3, $bucket_exists, $region) = $this->get_bucket_access($s3, $config, $bucket_name, $bucket_path);
634
635 if (!$bucket_exists) {
636 $iwp_backup_core->log("$whoweare Error: Failed to access bucket $bucket_name. Check your permissions and credentials.");
637 $iwp_backup_core->log(sprintf(__('%s Error: Failed to access bucket %s. Check your permissions and credentials.', 'iwp_backup_core'), $whoweare, $bucket_name), 'error');
638 return false;
639 }
640 }
641
642 $ret = true;
643
644 foreach ($files as $i => $file) {
645
646 if (preg_match("#^([^/]+)/(.*)$#", $orig_bucket_name, $bmatches)) {
647 $s3_bucket=$bmatches[1];
648 $s3_uri = $bmatches[2]."/".$file;
649 } else {
650 $s3_bucket = $orig_bucket_name;
651 $s3_uri = $file;
652 }
653 $iwp_backup_core->log("$whoweare: Delete remote: bucket=$s3_bucket, URI=$s3_uri");
654
655 $s3->setExceptions(true);
656 try {
657 if (!$s3->deleteObject($s3_bucket, $s3_uri)) {
658 $iwp_backup_core->log("$whoweare: Delete failed");
659 } elseif (null !== $this->quota_used && !empty($sizeinfo[$i]) && isset($config['quota']) && method_exists($this, 's3_record_quota_info')) {
660 $this->quota_used -= $sizeinfo[$i];
661 $this->s3_record_quota_info($this->quota_used, $config['quota']);
662 }
663 } catch (Exception $e) {
664 $iwp_backup_core->log("$whoweare delete failed: ".$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')');
665 $s3->setExceptions(false);
666 $ret = false;
667 }
668 $s3->setExceptions(false);
669
670 }
671
672 return $ret;
673
674 }
675
676 public function download($file) {
677
678 global $iwp_backup_core;
679
680 $config = $this->get_config();
681 $whoweare = $config['whoweare'];
682 $sse = empty($config['server_side_encryption']) ? false : true;
683
684 $s3 = $this->getS3(
685 $config['accesskey'],
686 $config['secretkey'],
687 IWP_MMB_Backup_Options::get_iwp_backup_option('IWP_ssl_useservercerts'),
688 IWP_MMB_Backup_Options::get_iwp_backup_option('IWP_ssl_disableverify'),
689 IWP_MMB_Backup_Options::get_iwp_backup_option('IWP_ssl_nossl'),
690 null,
691 $sse
692 );
693 if (is_wp_error($s3)) return $iwp_backup_core->log_wp_error($s3, false, true);
694
695 $bucket_name = untrailingslashit($config['path']);
696 $bucket_path = "";
697 if (!empty($config['as3_site_folder'])) {
698 $site_name = iwp_getSiteName();
699 $bucket_name.= '/'.untrailingslashit($site_name);
700 }
701 if (preg_match("#^([^/]+)/(.*)$#", $bucket_name, $bmatches)) {
702 $bucket_name = $bmatches[1];
703 $bucket_path = $bmatches[2]."/";
704 }
705
706
707 list($s3, $bucket_exists, $region) = $this->get_bucket_access($s3, $config, $bucket_name, $bucket_path);
708
709 if ($bucket_exists) {
710
711 $fullpath = $iwp_backup_core->backups_dir_location().'/'.$file;
712
713 $file_info = $this->listfiles($file);
714
715 if (is_array($file_info)) {
716 foreach ($file_info as $finfo) {
717 if ($finfo['name'] == $file) {
718 $file_size = $finfo['size'];
719 break;
720 }
721 }
722 }
723
724 if (!isset($file_size)) {
725 $iwp_backup_core->log("$whoweare Error: Failed to download $file. Check your permissions and credentials. Retrieved data: ".serialize($file_info));
726 $iwp_backup_core->log(sprintf(__('%s Error: Failed to download %s. Check your permissions and credentials.', 'iwp_backup_core'), $whoweare, $file), 'error');
727 return false;
728 }
729
730 return $iwp_backup_core->chunked_download($file, $this, $file_size, true, $s3, $this->download_chunk_size);
731
732
733 } else {
734 $iwp_backup_core->log("$whoweare Error: Failed to access bucket $bucket_name. Check your permissions and credentials.");
735 $iwp_backup_core->log(sprintf(__('%s Error: Failed to access bucket %s. Check your permissions and credentials.', 'InfiniteWP'), $whoweare, $bucket_name), 'error');
736 return false;
737 }
738 return true;
739
740 }
741
742 public function chunked_download($file, $headers, $s3, $fh) {
743
744 global $iwp_backup_core;
745
746 $resume = false;
747 $config = $this->get_config();
748 $whoweare = $config['whoweare'];
749
750 $bucket_name = untrailingslashit($config['path']);
751 if (!empty($config['as3_site_folder'])) {
752 $site_name = iwp_getSiteName();
753 $bucket_name.= '/'.untrailingslashit($site_name);
754 }
755 $bucket_path = "";
756
757 if (preg_match("#^([^/]+)/(.*)$#", $bucket_name, $bmatches)) {
758 $bucket_name = $bmatches[1];
759 $bucket_path = $bmatches[2]."/";
760 }
761
762 if (is_array($headers) && !empty($headers['Range']) && preg_match('/bytes=(\d+)-(\d+)$/', $headers['Range'], $matches)) {
763 $resume = $headers['Range'];
764 }
765
766 if (!$s3->getObject($bucket_name, $bucket_path.$file, $fh, $resume)) {
767 $iwp_backup_core->log("$whoweare Error: Failed to download $file. Check your permissions and credentials.");
768 $iwp_backup_core->log(sprintf(__('%s Error: Failed to download %s. Check your permissions and credentials.', 'InfiniteWP'), $whoweare, $file), 'error');
769 return false;
770 }
771
772 // This instructs the caller to look at the file pointer's position (i.e. ftell($fh)) to work out how many bytes were written.
773 return true;
774
775 }
776
777 public function config_print() {
778
779 // White: https://d36cz9buwru1tt.cloudfront.net/Powered-by-Amazon-Web-Services.jpg
780 $this->config_print_engine('s3', 'S3', 'Amazon S3', 'AWS', 'https://aws.amazon.com/console/', '<img src="//awsmedia.s3.amazonaws.com/AWS_logo_poweredby_black_127px.png" alt="Amazon Web Services">');
781
782 }
783
784 public function config_print_engine($key, $whoweare_short, $whoweare_long, $console_descrip, $console_url, $img_html = '', $include_endpoint_chooser = false) {
785
786 $opts = $this->get_config();
787
788 $use_s3_class = $this->indicate_s3_class();
789
790 if (!empty($include_endpoint_chooser)) {
791
792 if (is_array($include_endpoint_chooser)) {
793
794 $selected_endpoint = (!empty($opts['endpoint']) && in_array($opts['endpoint'], $include_endpoint_chooser)) ? $opts['endpoint'] : $include_endpoint_chooser[0];
795 }
796 }
797
798 }
799 /**
800 * This is not pretty, but is the simplest way to accomplish the task within the pre-existing structure (no need to re-invent the wheel of code with corner-cases debugged over years)
801 *
802 * @param object $s3 S3 Name
803 * @param string $bucket S3 Bucket
804 * @return boolean
805 */
806 public function use_dns_bucket_name($s3, $bucket) {
807 return is_a($s3, 'IWP_MMB_S3_Compat') ? true : $s3->useDNSBucketName(true, $bucket);
808 }
809
810 /**
811 * This method contains some repeated code. After getting an S3 object, it's time to see if we can access that bucket - either immediately, or via creating it, etc.
812 *
813 * @param object $s3 S3 name
814 * @param array $config array of config details
815 * @param string $bucket S3 Bucket
816 * @param string $path S3 Path
817 * @param boolean|string $endpoint S3 end point
818 * @return array
819 */
820 private function get_bucket_access($s3, $config, $bucket, $path, $endpoint = false) {
821
822 $bucket_exists = false;
823
824 if ('s3' == $config['key']) {
825
826 $s3->setExceptions(true);
827
828 if ('dreamobjects' == $config['key']) $this->set_region($s3, $endpoint);
829
830 try {
831 $region = @$s3->getBucketLocation($bucket);
832 // We want to distinguish between an empty region (null), and an exception or missing bucket (false)
833 if (empty($region) && false !== $region) $region = null;
834 } catch (Exception $e) {
835 $region = false;
836 }
837 $s3->setExceptions(false);
838 } else {
839 $region = 'n/a';
840 }
841
842 // See if we can detect the region (which implies the bucket exists and is ours), or if not create it
843 if (false === $region) {
844
845 $s3->setExceptions(true);
846 try {
847 if (@$s3->putBucket($bucket, 'private')) {
848 $bucket_exists = true;
849 }
850
851 } catch (Exception $e) {
852 $this->s3_exception = $e;
853 try {
854 if ('s3' == $config['key'] && $this->use_dns_bucket_name($s3, $bucket) && false !== @$s3->getBucket($bucket, $path, null, 1)) {
855 $bucket_exists = true;
856 }
857 } catch (Exception $e) {
858
859 // We don't put this in a separate catch block, since we need to be compatible with PHP 5.2 still
860 if (is_a($s3, 'IWP_MMB_S3_Compat') && is_a($e, 'Aws\S3\Exception\S3Exception')) {
861 $xml = $e->getResponse()->xml();
862
863 if (!empty($xml->Code) && 'AuthorizationHeaderMalformed' == $xml->Code && !empty($xml->Region)) {
864
865 $this->set_region($s3, $xml->Region);
866 $s3->setExceptions(false);
867
868 if (false !== @$s3->getBucket($bucket, $path, null, 1)) {
869 $bucket_exists = true;
870 }
871
872 } else {
873 $this->s3_exception = $e;
874 }
875 } else {
876 $this->s3_exception = $e;
877 }
878 }
879
880 }
881 $s3->setExceptions(false);
882
883 } else {
884 $bucket_exists = true;
885 }
886
887 if ($bucket_exists) {
888 if ('s3' != $config['key']) {
889 $this->set_region($s3, $endpoint, $bucket);
890 } elseif (!empty($region)) {
891 $this->set_region($s3, $region, $bucket);
892 }
893 }
894
895 return array($s3, $bucket_exists, $region);
896
897 }
898
899 }
900