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

openstack-base.php in UpdraftPlus: WP Backup & Migration Plugin 1.22.14, at methods/openstack-base.php

601 lines 22.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed.');
4
5 if (!class_exists('UpdraftPlus_BackupModule')) require_once(UPDRAFTPLUS_DIR.'/methods/backup-module.php');
6
7 class UpdraftPlus_BackupModule_openstack_base extends UpdraftPlus_BackupModule {
8
9 protected $chunk_size;
10
11 protected $client;
12
13 protected $method;
14
15 protected $desc;
16
17 protected $long_desc;
18
19 protected $img_url;
20
21 public function __construct($method, $desc, $long_desc = null, $img_url = '') {
22 $this->method = $method;
23 $this->desc = $desc;
24 $this->long_desc = (is_string($long_desc)) ? $long_desc : $desc;
25 $this->img_url = $img_url;
26 }
27
28 public function backup($backup_array) {
29
30 global $updraftplus;
31
32 $default_chunk_size = (defined('UPDRAFTPLUS_UPLOAD_CHUNKSIZE') && UPDRAFTPLUS_UPLOAD_CHUNKSIZE > 0) ? max(UPDRAFTPLUS_UPLOAD_CHUNKSIZE, 1048576) : 5242880;
33
34 $this->chunk_size = $updraftplus->jobdata_get('openstack_chunk_size', $default_chunk_size);
35
36 $opts = $this->get_options();
37
38 $this->container = $opts['path'];
39
40 try {
41 $storage = $this->get_openstack_service($opts, UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts'), UpdraftPlus_Options::get_updraft_option('updraft_ssl_disableverify'));
42 } catch (AuthenticationError $e) {
43 $updraftplus->log($this->desc.' authentication failed ('.$e->getMessage().')');
44 $updraftplus->log(sprintf(__('%s authentication failed', 'updraftplus'), $this->desc).' ('.$e->getMessage().')', 'error');
45 return false;
46 } catch (Exception $e) {
47 $updraftplus->log($this->desc.' error - failed to access the container ('.$e->getMessage().') (line: '.$e->getLine().', file: '.$e->getFile().')');
48 $updraftplus->log(sprintf(__('%s error - failed to access the container', 'updraftplus'), $this->desc).' ('.$e->getMessage().')', 'error');
49 return false;
50 }
51 // Get the container
52 try {
53 $this->container_object = $storage->getContainer($this->container);
54 } catch (Exception $e) {
55 $updraftplus->log('Could not access '.$this->desc.' container ('.get_class($e).', '.$e->getMessage().') (line: '.$e->getLine().', file: '.$e->getFile().')');
56 $updraftplus->log(sprintf(__('Could not access %s container', 'updraftplus'), $this->desc).' ('.get_class($e).', '.$e->getMessage().')', 'error');
57 return false;
58 }
59
60 foreach ($backup_array as $file) {
61
62 $file_key = 'status_'.md5($file);
63 $file_status = $this->jobdata_get($file_key, null, 'openstack_'.$file_key);
64 if (is_array($file_status) && !empty($file_status['chunks']) && !empty($file_status['chunks'][1]['size'])) $this->chunk_size = $file_status['chunks'][1]['size'];
65
66 // First, see the object's existing size (if any)
67 $uploaded_size = $this->get_remote_size($file);
68
69 try {
70 if (1 === $updraftplus->chunked_upload($this, $file, $this->method."://".$this->container."/$file", $this->desc, $this->chunk_size, $uploaded_size)) {
71 try {
72 if (false !== ($data = fopen($updraftplus->backups_dir_location().'/'.$file, 'r+'))) {
73 $this->container_object->uploadObject($file, $data);
74 $updraftplus->log($this->desc." regular upload: success");
75 $updraftplus->uploaded_file($file);
76 } else {
77 throw new Exception('uploadObject failed: fopen failed');
78 }
79 } catch (Exception $e) {
80 $this->log("regular upload: failed ($file) (".$e->getMessage().")");
81 $this->log("$file: ".sprintf(__('Error: Failed to upload', 'updraftplus')), 'error');
82 }
83 }
84 } catch (Exception $e) {
85 $updraftplus->log($this->desc.' error - failed to upload file'.' ('.$e->getMessage().') (line: '.$e->getLine().', file: '.$e->getFile().')');
86 $updraftplus->log(sprintf(__('%s error - failed to upload file', 'updraftplus'), $this->desc).' ('.$e->getMessage().')', 'error');
87 return false;
88 }
89 }
90
91 return array('object' => $this->container_object, 'orig_path' => $opts['path'], 'container' => $this->container);
92
93 }
94
95 private function get_remote_size($file) {
96 try {
97 $response = $this->container_object->getClient()->head($this->container_object->getUrl($file))->send();
98 $response_object = $this->container_object->dataObject()->populateFromResponse($response)->setName($file);
99 return $response_object->getContentLength();
100 } catch (Exception $e) {
101 // Allow caller to distinguish between zero-sized and not-found
102 return false;
103 }
104 }
105
106 /**
107 * This function lists the files found in the configured storage location
108 *
109 * @param String $match a substring to require (tested via strpos() !== false)
110 *
111 * @return Array - each file is represented by an array with entries 'name' and (optional) 'size'
112 */
113 public function listfiles($match = 'backup_') {
114 $opts = $this->get_options();
115 $container = $opts['path'];
116
117 if (empty($opts['user']) || (empty($opts['apikey']) && empty($opts['password']))) return new WP_Error('no_settings', __('No settings were found', 'updraftplus'));
118
119 try {
120 $storage = $this->get_openstack_service($opts, UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts'), UpdraftPlus_Options::get_updraft_option('updraft_ssl_disableverify'));
121 } catch (Exception $e) {
122 return new WP_Error('no_access', sprintf(__('%s error - failed to access the container', 'updraftplus'), $this->desc).' ('.$e->getMessage().')');
123 }
124
125 // Get the container
126 try {
127 $this->container_object = $storage->getContainer($container);
128 } catch (Exception $e) {
129 return new WP_Error('no_access', sprintf(__('%s error - failed to access the container', 'updraftplus'), $this->desc).' ('.$e->getMessage().')');
130 }
131
132 $results = array();
133 $marker = '';
134 $page_size = 1000;
135 try {
136 // http://php-opencloud.readthedocs.io/en/latest/services/object-store/objects.html#list-objects-in-a-container
137 while (null !== $marker) {
138
139 $params = array(
140 'prefix' => $match,
141 'limit' => $page_size,
142 'marker' => $marker
143 );
144
145 $objects = $this->container_object->objectList($params);
146
147 $total = $objects->count();
148
149 if (0 == $total) break;
150
151 $index = 0;
152
153 while (false !== ($file = $objects->offsetGet($index)) && !empty($file)) {
154 $index++;
155 try {
156 if ((is_object($file) && !empty($file->name))) {
157 $result = array('name' => $file->name);
158 // Rackspace returns the size of a manifested file properly; other OpenStack implementations may not
159 if (!empty($file->bytes)) {
160 $result['size'] = $file->bytes;
161 } else {
162 $size = $this->get_remote_size($file->name);
163 if (false !== $size && $size > 0) $result['size'] = $size;
164 }
165 $results[] = $result;
166 }
167 } catch (Exception $e) {
168 // Catch
169 }
170 $marker = (!empty($file->name) && $total >= $page_size) ? $file->name : null;
171 }
172
173 }
174 } catch (Exception $e) {
175 // Catch
176 }
177
178 return $results;
179 }
180
181 /**
182 * Called when all chunks have been uploaded, to allow any required finishing actions to be carried out
183 *
184 * @param String $file - the basename of the file being uploaded
185 *
186 * @return Boolean - success or failure state of any finishing actions
187 */
188 public function chunked_upload_finish($file) {
189
190 $chunk_path = 'chunk-do-not-delete-'.$file;
191 try {
192
193 $headers = array(
194 'Content-Length' => 0,
195 'X-Object-Manifest' => sprintf('%s/%s', $this->container, $chunk_path.'_')
196 );
197
198 $url = $this->container_object->getUrl($file);
199 $this->container_object->getClient()->put($url, $headers)->send();
200 return true;
201
202 } catch (Exception $e) {
203 global $updraftplus;
204 $updraftplus->log("Error when sending manifest (".get_class($e)."): ".$e->getMessage());
205 return false;
206 }
207 }
208
209 /**
210 * N.B. Since we use varying-size chunks, we must be careful as to what we do with $chunk_index
211 *
212 * @param String $file Full path for the file being uploaded
213 * @param Resource $fp File handle to read upload data from
214 * @param Integer $chunk_index Index of chunked upload
215 * @param Integer $upload_size Size of the upload, in bytes
216 * @param Integer $upload_start How many bytes into the file the upload process has got
217 * @param Integer $upload_end How many bytes into the file we will be after this chunk is uploaded
218 * @param Integer $total_file_size Total file size
219 *
220 * @return Boolean
221 */
222 public function chunked_upload($file, $fp, $chunk_index, $upload_size, $upload_start, $upload_end, $total_file_size) {
223
224 global $updraftplus;
225
226 $file_key = 'status_'.md5($file);
227 $file_status = $this->jobdata_get($file_key, null, 'openstack_'.$file_key);
228
229 $next_chunk_size = $upload_size;
230
231 $bytes_already_uploaded = 0;
232
233 $last_uploaded_chunk_index = 0;
234
235 // Once a chunk is uploaded, its status is set, allowing the sequence to be reconstructed
236 if (is_array($file_status) && isset($file_status['chunks']) && !empty($file_status['chunks'])) {
237 foreach ($file_status['chunks'] as $c_id => $c_status) {
238 if ($c_id > $last_uploaded_chunk_index) $last_uploaded_chunk_index = $c_id;
239 if ($chunk_index + 1 == $c_id) {
240 $next_chunk_size = $c_status['size'];
241 }
242 $bytes_already_uploaded += $c_status['size'];
243 }
244 } else {
245 $file_status = array('chunks' => array());
246 }
247
248 $this->jobdata_set($file_key, $file_status);
249
250 if ($upload_start < $bytes_already_uploaded) {
251 if ($next_chunk_size != $upload_size) {
252 $response = new stdClass;
253 $response->new_chunk_size = $upload_size;
254 $response->log = false;
255 return $response;
256 } else {
257 return 1;
258 }
259 }
260
261 // Shouldn't be able to happen
262 if ($chunk_index <= $last_uploaded_chunk_index) {
263 $updraftplus->log($this->desc.": Chunk sequence error; chunk_index=$chunk_index, last_uploaded_chunk_index=$last_uploaded_chunk_index, upload_start=$upload_start, upload_end=$upload_end, file_status=".json_encode($file_status));
264 }
265
266 // Used to use $chunk_index here, before switching to variable chunk sizes
267 $upload_remotepath = 'chunk-do-not-delete-'.$file.'_'.sprintf("%016d", $chunk_index);
268
269 $remote_size = $this->get_remote_size($upload_remotepath);
270
271 // Without this, some versions of Curl add Expect: 100-continue, which results in Curl then giving this back: curl error: 55) select/poll returned error
272 // Didn't make the difference - instead we just check below for actual success even when Curl reports an error
273 // $chunk_object->headers = array('Expect' => '');
274
275 if ($remote_size >= $upload_size) {
276 $updraftplus->log($this->desc.": Chunk ($upload_start - $upload_end, $chunk_index): already uploaded");
277 } else {
278 $updraftplus->log($this->desc.": Chunk ($upload_start - $upload_end, $chunk_index): begin upload");
279 // Upload the chunk
280 try {
281 $data = fread($fp, $upload_size);
282 $time_start = microtime(true);
283 $this->container_object->uploadObject($upload_remotepath, $data);
284 $time_now = microtime(true);
285 $time_taken = $time_now - $time_start;
286 if ($next_chunk_size < 52428800 && $total_file_size > 0 && $upload_end + 1 < $total_file_size) {
287 $job_run_time = $time_now - $updraftplus->job_time_ms;
288 if ($time_taken < 10) {
289 $upload_rate = $upload_size / max($time_taken, 0.0001);
290 $upload_secs = min(floor($job_run_time), 10);
291 if ($job_run_time < 15) $upload_secs = max(6, $job_run_time*0.6);
292
293 // In megabytes
294 $memory_limit_mb = $updraftplus->memory_check_current();
295 $bytes_used = memory_get_usage();
296 $bytes_free = $memory_limit_mb * 1048576 - $bytes_used;
297
298 $new_chunk = max(min($upload_secs * $upload_rate * 0.9, 52428800, $bytes_free), 5242880);
299 $new_chunk = $new_chunk - ($new_chunk % 5242880);
300 $next_chunk_size = (int) $new_chunk;
301 $updraftplus->jobdata_set('openstack_chunk_size', $next_chunk_size);
302 }
303 }
304
305 } catch (Exception $e) {
306 $updraftplus->log($this->desc." chunk upload: error: ($file / $chunk_index) (".$e->getMessage().") (line: ".$e->getLine().', file: '.$e->getFile().')');
307 // Experience shows that Curl sometimes returns a select/poll error (curl error 55) even when everything succeeded. Google seems to indicate that this is a known bug.
308
309 $remote_size = $this->get_remote_size($upload_remotepath);
310
311 if ($remote_size >= $upload_size) {
312 $updraftplus->log("$file: Chunk now exists; ignoring error (presuming it was an apparently known curl bug)");
313 } else {
314 $updraftplus->log("$file: ".sprintf(__('%s Error: Failed to upload', 'updraftplus'), $this->desc), 'error');
315 return false;
316 }
317 }
318 }
319
320 $file_status['chunks'][$chunk_index]['size'] = $upload_size;
321
322 $this->jobdata_set($file_key, $file_status);
323
324 if ($next_chunk_size != $upload_size) {
325 $response = new stdClass;
326 $response->new_chunk_size = $next_chunk_size;
327 $response->log = true;
328 return $response;
329 }
330
331 return true;
332 }
333
334 /**
335 * Delete a single file from the service using OpenStack API
336 *
337 * @param Array|String $files - array of file names to delete
338 * @param Array $data - service object and container details
339 * @param Array $sizeinfo - unused here
340 * @return Boolean|String - either a boolean true or an error code string
341 */
342 public function delete($files, $data = false, $sizeinfo = array()) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $sizeinfo unused
343
344 global $updraftplus;
345 if (is_string($files)) $files = array($files);
346
347 if (is_array($data)) {
348 $container_object = $data['object'];
349 $container = $data['container'];
350 } else {
351 $opts = $this->get_options();
352 $container = $opts['path'];
353 try {
354 $storage = $this->get_openstack_service($opts, UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts'), UpdraftPlus_Options::get_updraft_option('updraft_ssl_disableverify'));
355 } catch (AuthenticationError $e) {
356 $updraftplus->log($this->desc.' authentication failed ('.$e->getMessage().')');
357 $updraftplus->log(sprintf(__('%s authentication failed', 'updraftplus'), $this->desc).' ('.$e->getMessage().')', 'error');
358 return 'authentication_fail';
359 } catch (Exception $e) {
360 $updraftplus->log($this->desc.' error - failed to access the container ('.$e->getMessage().')');
361 $updraftplus->log(sprintf(__('%s error - failed to access the container', 'updraftplus'), $this->desc).' ('.$e->getMessage().')', 'error');
362 return 'service_unavailable';
363 }
364 // Get the container
365 try {
366 $container_object = $storage->getContainer($container);
367 } catch (Exception $e) {
368 $updraftplus->log('Could not access '.$this->desc.' container ('.get_class($e).', '.$e->getMessage().')');
369 $updraftplus->log(sprintf(__('Could not access %s container', 'updraftplus'), $this->desc).' ('.get_class($e).', '.$e->getMessage().')', 'error');
370 return 'container_access_error';
371 }
372
373 }
374
375 $ret = true;
376 foreach ($files as $file) {
377
378 $updraftplus->log($this->desc.": Delete remote: container=$container, path=$file");
379
380 // We need to search for chunks
381 $chunk_path = "chunk-do-not-delete-".$file;
382
383 try {
384 $objects = $container_object->objectList(array('prefix' => $chunk_path));
385 $index = 0;
386 while (false !== ($chunk = $objects->offsetGet($index)) && !empty($chunk)) {
387 try {
388 $name = $chunk->name;
389 $container_object->dataObject()->setName($name)->delete();
390 $updraftplus->log($this->desc.': Chunk deleted: '.$name);
391 } catch (Exception $e) {
392 $updraftplus->log($this->desc." chunk delete failed: $name: ".$e->getMessage());
393 }
394 $index++;
395 }
396 } catch (Exception $e) {
397 $updraftplus->log($this->desc.' chunk delete failed: '.$e->getMessage());
398 }
399
400 // Finally, delete the object itself
401 try {
402 $container_object->dataObject()->setName($file)->delete();
403 $updraftplus->log($this->desc.': Deleted: '.$file);
404 } catch (Exception $e) {
405 $updraftplus->log($this->desc.' delete failed: '.$e->getMessage());
406 $ret = 'file_delete_error';
407 }
408 }
409 return $ret;
410 }
411
412 public function download($file) {
413
414 global $updraftplus;
415
416 $opts = $this->get_options();
417
418 try {
419 $storage = $this->get_openstack_service($opts, UpdraftPlus_Options::get_updraft_option('updraft_ssl_useservercerts'), UpdraftPlus_Options::get_updraft_option('updraft_ssl_disableverify'));
420 } catch (AuthenticationError $e) {
421 $updraftplus->log($this->desc.' authentication failed ('.$e->getMessage().')');
422 $updraftplus->log(sprintf(__('%s authentication failed', 'updraftplus'), $this->desc).' ('.$e->getMessage().')', 'error');
423 return false;
424 } catch (Exception $e) {
425 $updraftplus->log($this->desc.' error - failed to access the container ('.$e->getMessage().')');
426 $updraftplus->log(sprintf(__('%s error - failed to access the container', 'updraftplus'), $this->desc).' ('.$e->getMessage().')', 'error');
427 return false;
428 }
429
430 $container = untrailingslashit($opts['path']);
431 $updraftplus->log($this->desc." download: ".$this->method."://$container/$file");
432
433 // Get the container
434 try {
435 $this->container_object = $storage->getContainer($container);
436 } catch (Exception $e) {
437 $updraftplus->log('Could not access '.$this->desc.' container ('.get_class($e).', '.$e->getMessage().')');
438 $updraftplus->log(sprintf(__('Could not access %s container', 'updraftplus'), $this->desc).' ('.get_class($e).', '.$e->getMessage().')', 'error');
439 return false;
440 }
441
442 // Get information about the object within the container
443 $remote_size = $this->get_remote_size($file);
444 if (false === $remote_size) {
445 $updraftplus->log('Could not access '.$this->desc.' object');
446 $updraftplus->log(sprintf(__('The %s object was not found', 'updraftplus'), $this->desc), 'error');
447 return false;
448 }
449
450 return (!is_bool($remote_size)) ? $updraftplus->chunked_download($file, $this, $remote_size, true, $this->container_object) : false;
451
452 }
453
454 public function chunked_download($file, $headers, $container_object) {
455 try {
456 $dl = $container_object->getObject($file, $headers);
457 } catch (Exception $e) {
458 global $updraftplus;
459 $updraftplus->log("$file: Failed to download (".$e->getMessage().")");
460 $updraftplus->log("$file: ".sprintf(__("%s Error", 'updraftplus'), $this->desc).": ".__('Error downloading remote file: Failed to download', 'updraftplus').' ('.$e->getMessage().")", 'error');
461 return false;
462 }
463 return $dl->getContent();
464 }
465
466 public function credentials_test_go($opts, $path, $useservercerts, $disableverify) {
467
468 if (preg_match("#^([^/]+)/(.*)$#", $path, $bmatches)) {
469 $container = $bmatches[1];
470 $path = $bmatches[2];
471 } else {
472 $container = $path;
473 $path = '';
474 }
475
476 if (empty($container)) {
477 _e('Failure: No container details were given.', 'updraftplus');
478 return;
479 }
480
481 try {
482 $storage = $this->get_openstack_service($opts, $useservercerts, $disableverify);
483 // @codingStandardsIgnoreLine
484 } catch (Guzzle\Http\Exception\ClientErrorResponseException $e) {
485 $response = $e->getResponse();
486 $code = $response->getStatusCode();
487 $reason = $response->getReasonPhrase();
488 if (401 == $code && 'Unauthorized' == $reason) {
489 echo __('Authorisation failed (check your credentials)', 'updraftplus');
490 } else {
491 echo __('Authorisation failed (check your credentials)', 'updraftplus')." ($code:$reason)";
492 }
493 return;
494 } catch (AuthenticationError $e) {
495 echo sprintf(__('%s authentication failed', 'updraftplus'), $this->desc).' ('.$e->getMessage().')';
496 return;
497 } catch (Exception $e) {
498 echo sprintf(__('%s authentication failed', 'updraftplus'), $this->desc).' ('.get_class($e).', '.$e->getMessage().')';
499 return;
500 }
501
502 try {
503 $container_object = $storage->getContainer($container);
504 // @codingStandardsIgnoreLine
505 } catch (Guzzle\Http\Exception\ClientErrorResponseException $e) {
506 $response = $e->getResponse();
507 $code = $response->getStatusCode();
508 $reason = $response->getReasonPhrase();
509 if (404 == $code) {
510 $container_object = $storage->createContainer($container);
511 } else {
512 echo __('Authorisation failed (check your credentials)', 'updraftplus')." ($code:$reason)";
513 return;
514 }
515 } catch (Exception $e) {
516 echo sprintf(__('%s authentication failed', 'updraftplus'), $this->desc).' ('.get_class($e).', '.$e->getMessage().')';
517 return;
518 }
519
520 if (!is_a($container_object, 'OpenCloud\ObjectStore\Resource\Container') && !is_a($container_object, 'Container')) {
521 echo sprintf(__('%s authentication failed', 'updraftplus'), $this->desc).' ('.get_class($container_object).')';
522 return;
523 }
524
525 $try_file = md5(rand()).'.txt';
526
527 try {
528 $object = $container_object->uploadObject($try_file, 'UpdraftPlus test file', array('content-type' => 'text/plain'));
529 } catch (Exception $e) {
530 echo sprintf(__('%s error - we accessed the container, but failed to create a file within it', 'updraftplus'), $this->desc).' ('.get_class($e).', '.$e->getMessage().')';
531 if (!empty($this->region)) echo ' '.sprintf(__('Region: %s', 'updraftplus'), $this->region);
532 return;
533 }
534
535 echo __('Success', 'updraftplus').": ".__('We accessed the container, and were able to create files within it.', 'updraftplus');
536 if (!empty($this->region)) echo ' '.sprintf(__('Region: %s', 'updraftplus'), $this->region);
537
538 try {
539 if (!empty($object)) {
540 // One OpenStack server we tested on did not delete unless we slept... some kind of race condition at their end
541 sleep(1);
542 $object->delete();
543 }
544 } catch (Exception $e) {
545 // Catch
546 }
547
548 }
549
550 /**
551 * Get the pre configuration template
552 *
553 * @return String - the template
554 */
555 public function get_pre_configuration_template() {
556
557 global $updraftplus_admin;
558
559 $classes = $this->get_css_classes(false);
560
561 ?>
562 <tr class="<?php echo $classes . ' ' . $this->method . '_pre_config_container';?>">
563 <td colspan="2">
564 <?php
565 if (!empty($this->img_url)) {
566 ?>
567 <img alt="<?php echo $this->long_desc; ?>" src="<?php echo UPDRAFTPLUS_URL.$this->img_url; ?>">
568 <?php
569 }
570 ?>
571 <br>
572 <?php
573 // Check requirements.
574 global $updraftplus_admin;
575 if (!function_exists('mb_substr')) {
576 $updraftplus_admin->show_double_warning('<strong>'.__('Warning', 'updraftplus').':</strong> '.sprintf(__('Your web server\'s PHP installation does not included a required module (%s). Please contact your web hosting provider\'s support.', 'updraftplus'), 'mbstring').' '.sprintf(__("UpdraftPlus's %s module <strong>requires</strong> %s. Please do not file any support requests; there is no alternative.", 'updraftplus'), $this->desc, 'mbstring'), $this->method);
577 }
578 $updraftplus_admin->curl_check($this->long_desc, false, $this->method);
579 echo '<br>';
580 $this->get_pre_configuration_middlesection_template();
581 ?>
582 </td>
583 </tr>
584
585 <?php
586 }
587
588 /**
589 * Get the configuration template
590 *
591 * @return String - the template, ready for substitutions to be carried out
592 */
593 public function get_configuration_template() {
594 ob_start();
595 $template_str = ob_get_clean();
596 $template_str .= $this->get_configuration_middlesection_template();
597 $template_str .= $this->get_test_button_html($this->desc);
598 return $template_str;
599 }
600 }
601