<?php
// @codingStandardsIgnoreStart
/*
UpdraftPlus Addon: backblaze:Backblaze Support
Description: Backblaze Support
Version: 1.3
Shop: /shop/backblaze/
Include: includes/backblaze
IncludePHP: methods/addon-base-v2.php
RequiresPHP: 5.3.3
Latest Change: 1.15.3
*/
// @codingStandardsIgnoreEnd
if (!defined('UPDRAFTPLUS_DIR')) die('No direct access allowed');
if (!class_exists('UpdraftPlus_RemoteStorage_Addons_Base_v2')) require_once(UPDRAFTPLUS_DIR.'/methods/addon-base-v2.php');
/**
* Possible enhancements:
* - Investigate porting to WP HTTP API so that curl is not required
*/
class UpdraftPlus_Addons_RemoteStorage_backblaze extends UpdraftPlus_RemoteStorage_Addons_Base_v2 {
private $_large_file_id;
private $_sha1_of_parts;
private $_uploaded_size;
private $chunk_size = 5242880;
/**
* Constructor
*/
public function __construct() {
// 3rd parameter: chunking? 4th: Test button?
parent::__construct('backblaze', 'Backblaze B2', true, true);
// Set it any lower, any you will get an error when calling /b2_finish_large_file upon finishing: 400, Message: Part number 1 is smaller than 5000000 bytes"
if (defined('UPDRAFTPLUS_UPLOAD_CHUNKSIZE') && UPDRAFTPLUS_UPLOAD_CHUNKSIZE > 0) $this->chunk_size = max(UPDRAFTPLUS_UPLOAD_CHUNKSIZE, 5000000);
}
/**
* Upload a single file
*
* @param String $file - the basename of the file to upload
* @param String $local_path - the full path of the file
*
* @return Boolean - success status. Failures can also be thrown as exceptions.
*/
public function do_upload($file, $local_path) {
global $updraftplus;
$opts = $this->options;
$storage = $this->get_storage();
if (is_wp_error($storage)) throw new Exception($storage->get_error_message());
if (!is_object($storage)) throw new Exception("Backblaze service error (got a ".gettype($storage).")");
$backup_path = empty($opts['backup_path']) ? '' : trailingslashit($opts['backup_path']);
$remote_path = $backup_path.$file;
$file_hash = md5($file);
$this->_uploaded_size = $this->jobdata_get('total_bytes_sent_'.$file_hash, 0);
if (!file_exists($local_path) || !is_readable($local_path)) throw new Exception("Could not read file: $local_path");
$bucket_name = $opts['bucket_name'];
// Create bucket if bucket doesn't exists
if (!isset($this->is_upload_bucket_exist) && $this->is_valid_bucket_name($bucket_name)) {
$buckets = $this->get_bucket_names_array();
if (!in_array($bucket_name, $buckets)) {
$new_bucket_created = $storage->createPrivateBucket($bucket_name);
if ($new_bucket_created) {
$this->is_upload_bucket_exist = true;
$this->log("bucket was not found, but a new private bucket has now been created: ".$bucket_name);
} else {
$this->log("bucket was not found, and creation of a new private bucket failed: ".$bucket_name);
}
} else {
$this->is_upload_bucket_exist = true;
}
}
if (1 === ($ret = $updraftplus->chunked_upload($this, $file, "backblaze://".trailingslashit($bucket_name).$backup_path.$file, 'Backblaze', $this->chunk_size, $this->_uploaded_size))) {
$result = $storage->upload(array(
'BucketName' => $opts['bucket_name'],
'FileName' => $remote_path,
'Body' => file_get_contents($local_path),
));
if (is_object($result) && is_callable(array($result, 'getSize')) && $result->getSize() > 1) {
$ret = true;
} else {
$ret = false;
$this->log("all-in-one upload fail: ".serialize($result));
}
}
return $ret;
}
/**
* N.B. If we ever use varying-size chunks, we must be careful as to what we do with $chunk_index
*
* @param String $file - Basename for the file being uploaded
* @param Resource|String $fp - Data to send, or a file handle to read upload data from
* @param Integer $chunk_index - Index of chunked upload
* @param Integer $upload_size - Size of the upload, in bytes (this and the next are only used if a resource was given for $fp)
* @param Integer $upload_start - How many bytes into the file the upload process has got
* @param Integer $upload_end - How many bytes into the file we will be after this chunk is uploaded (not currently used)
* @param Integer $total_file_size - Total file size (not currently used)
*
* @return Boolean|WP_Error
*/
public function chunked_upload($file, $fp, $chunk_index, $upload_size = 0, $upload_start = 0, $upload_end = 0, $total_file_size = 0) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Filter use
// Already done? This is not checked if we are sent data directly, as that implies forcing.
if (is_resource($fp) && $upload_start < $this->_uploaded_size) return 1;
$storage = $this->get_storage();
if (is_wp_error($storage)) return $storage;
if (!is_object($storage)) return new WP_Error('no_backblaze_service', "Backblaze service error (got a ".gettype($storage).")");
$file_hash = md5($file);
$upload_state = $this->jobdata_get('upload_state_'.$file_hash, array());
// An upload URL is valid for 24 hours. But, we'll only use them for 1 hour, in case something else happens to invalidate it (we don't want to wait a whole day before getting a new one).
if (!empty($upload_state['saved_at']) && $upload_state['saved_at'] < time() - 3600) $upload_state = array();
$large_file_id = empty($upload_state['large_file_id']) ? false : $upload_state['large_file_id'];
$upload_url = empty($upload_state['upload_url']) ? false : $upload_state['upload_url'];
$auth_token = empty($upload_state['auth_token']) ? false : $upload_state['auth_token'];
$need_new_state = ($large_file_id && $upload_url && $auth_token) ? false : true;
$opts = $this->options;
$backup_path = empty($opts['backup_path']) ? '' : trailingslashit($opts['backup_path']);
$remote_path = $backup_path.$file;
if (!$large_file_id) {
$this->log("initiating multi-part upload");
try {
$response = $storage->uploadLargeStart(array(
'FileName' => $remote_path,
'BucketName' => $opts['bucket_name'],
));
if (empty($response['fileId'])) {
$this->log('Unexpected response to uploadLargeStart: '.serialize($response));
return false;
}
} catch (Exception $e) {
$this->log('Unexpected chunk uploading exception ('.get_class($e).'): '.$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')');
return false;
}
$large_file_id = $response['fileId'];
}
$this->_large_file_id = $large_file_id;
if (!$upload_url || !$auth_token) {
try {
$this->log("requesting multi-part file upload url (id $large_file_id)");
$response = $storage->uploadLargeUrl(array(
'FileId' => $large_file_id,
));
if (empty($response['authorizationToken']) || empty($response['uploadUrl'])) {
$this->log('Unexpected response to uploadLargeUrl: '.serialize($response));
return false;
}
} catch (Exception $e) {
$this->log('Unexpected error when getting upload URL ('.get_class($e).'): '.$e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')');
return false;
}
$auth_token = $response['authorizationToken'];
$upload_url = $response['uploadUrl'];
}
if ($need_new_state) {
$this->jobdata_set('upload_state_'.$file_hash, array(
'large_file_id' => $large_file_id,
'upload_url' => $upload_url,
'auth_token' => $auth_token,
// N.B. An upload URL is valid for 24 hours
'saved_at' => time()
));
}
if (is_resource($fp)) {
if (false === ($data = fread($fp, $upload_size))) {
$this->log(__('Error: unexpected file read fail', 'updraftplus'), 'error');
$this->log("File read fail (fread() returned false)");
return false;
}
} elseif (is_string($fp)) {
$data = $fp;
} else {
return new WP_Error('backblaze_chunk_data_error', __('Error:', 'updraftplus')." backblaze::chunked_upload() received invalid input");
}
$sha1_of_parts = $this->jobdata_get('sha1_of_parts_'.$file_hash, array());
$sha1_of_parts[$chunk_index - 1] = sha1($data);
try {
$response = $storage->uploadLargePart(array(
'AuthorizationToken' => $auth_token,
'FilePartNo' => $chunk_index,
'UploadUrl' => $upload_url,
'Body' => $data,
));
if (!is_array($response) || !isset($response['partNumber'])) {
$this->log("Unexpected response to uploadLargePart: ".serialize($response));
return false;
}
} catch (Exception $e) {
if ($e->getCode() >= 500 && $e->getCode() <= 599) {
$this->jobdata_set('upload_state_'.$file_hash, array(
'large_file_id' => $large_file_id,
'upload_url' => '',
'auth_token' => '',
));
}
return new WP_Error('backblaze_chunk_upload_error', __('Error:', 'updraftplus')." {$e->getCode()}, Message: {$e->getMessage()}");
}
$this->_sha1_of_parts = $sha1_of_parts;
$this->jobdata_set('sha1_of_parts_'.$file_hash, $sha1_of_parts);
$this->jobdata_set('total_bytes_sent_'.$file_hash, $upload_end + 1);
return true;
}
/**
* Called when all chunks have been uploaded, to allow any required finishing actions to be carried out
*
* @param String $file - the basename of the file being uploaded
*
* @return Integer|Boolean - success or failure state of any finishing actions
*/
public function chunked_upload_finish($file) {
$file_hash = md5($file);
$storage = $this->get_storage();
// This happens if chunked_upload_finish is called without chunked_upload having been called
if (empty($this->_large_file_id)) {
$upload_state = $this->jobdata_get('upload_state_'.$file_hash, array());
// An upload URL is valid for 24 hours. But, we'll only use them for 1 hour, in case something else happens to invalidate it (we don't want to wait a whole day before getting a new one).
if (!empty($upload_state['saved_at']) && $upload_state['saved_at'] < time() - 3600) $upload_state = array();
$this->_large_file_id = empty($upload_state['large_file_id']) ? false : $upload_state['large_file_id'];
$this->_sha1_of_parts = $this->jobdata_get('sha1_of_parts_'.$file_hash, array());
}
try {
$response = $storage->uploadLargeFinish(array(
'FileId' => $this->_large_file_id,
'FilePartSha1Array' => $this->_sha1_of_parts,
));
} catch (Exception $e) {
global $updraftplus;
if (preg_match('/No active upload for: .*/', $e->getMessage())) {
$this->log("upload: b2_finish_large_file has already been called ('".$e->getMessage()."')");
return 1;
} elseif (preg_match('/Part number (\d+) has not been uploaded/i', $e->getMessage(), $matches)) {
$missing_chunk_index = $matches[1];
$this->log("Exception in uploadLargeFinish(); will retry part $missing_chunk_index: {$e->getCode()}, Message: {$e->getMessage()} (line: {$e->getLine()}, file: {$e->getFile()})");
$updraft_dir = $updraftplus->backups_dir_location();
// If more than this are needed, they will happen on the next resumption
static $retries = 12;
if (false === ($data = file_get_contents($updraft_dir.'/'.$file, false, null, ($missing_chunk_index - 1 ) * $this->chunk_size, $this->chunk_size))) {
$retry_part = new WP_Error('file_read_failed', "Could not read: $file");
} elseif ($retries > 0) {
$retries--;
$retry_part = $this->chunked_upload($file, $data, $missing_chunk_index);
// Missing part was uploaded; try the whole again
if (true === $retry_part) {
return $this->chunked_upload_finish($file);
}
// N.B. chunked_upload() does its own logging when returning false
}
if (is_wp_error($retry_part)) {
$this->log("Failed ".$retry_part->get_error_code().": ".$retry_part->get_error_message());
}
} else {
$this->log("Exception in uploadLargeFinish(): {$e->getCode()}, Message: {$e->getMessage()} (line: {$e->getLine()}, file: {$e->getFile()})");
}
return false;
}
global $updraftplus;
$this->log('upload: success (b2_finish_large_file called successfully; chunks='.count($this->_sha1_of_parts).', file ID returned='.$response->getId().', size='.$response->getSize().')');
// Clean-up
$this->jobdata_delete('upload_state_'.$file_hash);
$this->jobdata_delete('sha1_of_parts_'.$file_hash);
// (int)1 means 'we already logged', as opposed to (boolean)true which does not
return 1;
}
/**
* Perform a download of the requested file
*
* @param String $file - the file (basename) to download
* @param String $fullpath - the full path to download it too
* @param Integer $start_offset - byte marker to begin at (starting from 0)
*
* @return Boolean|Integer - success/failure, or a byte counter of how much has been downloaded. Exceptions can also be thrown for errors.
*/
public function do_download($file, $fullpath) {// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Filter use
global $updraftplus;
$remote_files = $this->do_listfiles($file);
if (is_wp_error($remote_files)) {
throw new Exception('Download error ('.$remote_files->get_error_code().'): '.$remote_files->get_error_message());
}
foreach ($remote_files as $file_info) {
if ($file_info['name'] == $file) {
return $updraftplus->chunked_download($file, $this, $file_info['size'], true, null, 2*1048576);
}
}
$this->log("$file: file not found in listing of remote directory");
return false;
}
/**
* Callback used by by chunked downloading API
*
* @param String $file - the file (basename) to be downloaded
* @param Array $headers - supplied headers
* @return String - the data downloaded
*/
public function chunked_download($file, $headers) {
// $curl_options = array();
// if (is_array($headers) && !empty($headers['Range']) && preg_match('/bytes=(.*)$/', $headers['Range'], $matches)) {
// $curl_options[CURLOPT_RANGE] = $matches[1];
$opts = $this->options;
$storage = $this->get_storage();
$backup_path = empty($opts['backup_path']) ? '' : trailingslashit($opts['backup_path']);
$options = array(
'BucketName' => $opts['bucket_name'],
'FileName' => $backup_path.$file,
);
if (!empty($headers)) $options['headers'] = $headers;
$remote_file = $storage->download($options);
return is_string($remote_file) ? $remote_file : false;
}
/**
* Acts as a WordPress options filter
*
* @param Array $settings - pre-filtered settings
*
* @return Array filtered settings
*/
public function options_filter($settings) {
if (is_array($settings) && !empty($settings['version']) && !empty($settings['settings'])) {
foreach ($settings['settings'] as $instance_id => $instance_settings) {
if (!empty($instance_settings['backup_path'])) {
$settings['settings'][$instance_id]['backup_path'] = trim($instance_settings['backup_path'], "/ \t\n\r\0\x0B");
}
}
}
return $settings;
}
/**
* Delete an indicated file from remote storage
*
* @param Array $files - the files (basename) to delete
*
* @return Boolean|Array - success/failure status of the delete operation. Throwing exception is also permitted.
*/
public function do_delete($files) {
$opts = $this->options;
$storage = $this->get_storage();
$backup_path = empty($opts['backup_path']) ? '' : trailingslashit($opts['backup_path']);
try {
if (count($files) > 1) {
$multipleFiles = array();
foreach ($files as $file) {
$multipleFiles[] = array(
'FileName' => $backup_path.$file,
'BucketName' => $opts['bucket_name']
);
}
$result = $storage->deleteMultipleFiles($multipleFiles, $opts['bucket_name'], $backup_path);
} else {
$fileName = $files[0];
$result = $storage->deleteFile(array(
'FileName' => $backup_path.$fileName,
'BucketName' => $opts['bucket_name'],
));
}
} catch (UpdraftPlus_Backblaze_NotFoundException $e) {
// This exception should only be possible on the single file delete path
$this->log("$fileName: file not found (so likely already deleted)");
return true;
}
return $result;
}
/**
* This method is used to get a list of backup files for the remote storage option
*
* @param String $match - a string to match when looking for files
*
* @return Array|WP_Error - returns an array of files (arrays with keys 'name' (basename) and (optional) 'size' (in bytes)) or a WordPress error. Throwing an exception is also allowed.
*/
public function do_listfiles($match = 'backup_') {
$opts = $this->get_options();
$storage = $this->get_storage();
// When listing, paths in the root must not begin with a slash
$backup_path = empty($opts['backup_path']) ? '' : trailingslashit($opts['backup_path']);
try {
$remote_files = $storage->listFiles(array(
'BucketName' => $opts['bucket_name'],
'Prefix' => $backup_path.$match
));
} catch (Exception $e) {
return new WP_Error('backblaze_list_error', $e->getMessage().' (line: '.$e->getLine().', file: '.$e->getFile().')');
}
if (is_wp_error($remote_files)) return $remote_files;
$files = array();
foreach ($remote_files as $file) {
$file_name = $file->getName();
if ($backup_path && 0 !== strpos($file_name, $backup_path)) continue;
$files[] = array(
'name' => substr($file_name, strlen($backup_path)),
'size' => $file->getSize(),
// 'fid' => $file->getId(),
);
}
return $files;
}
/**
* Get a list of parameters required to be present for a credential tests, plus descriptions
*
* @return Array
*/
public function get_credentials_test_required_parameters() {
return array(
'account_id' => __('Account ID', 'updraftplus'),
'key' => __('Account Key', 'updraftplus')
);
}
/**
* Run a credentials test. Output can be echoed.
*
* @param String $testfile - basename to use for the test
* @param Array $posted_settings - settings to use
*
* @return Array - 'result' indicating a success/failure status, and 'data' with returned data
*/
protected function do_credentials_test($testfile, $posted_settings = array()) {
$bucket_name = $posted_settings['bucket_name'];
$result = false;
$data = null;
$storage = $this->get_storage();
try {
if (!$this->is_valid_bucket_name($bucket_name)) {
echo __('Invalid bucket name', 'updraftplus')."\n";
} else {
$buckets = $this->get_bucket_names_array();
$new_bucket_created = false;
if (!in_array($bucket_name, $buckets)) {
$new_bucket_created = $storage->createPrivateBucket($bucket_name);
}
if (in_array($bucket_name, $buckets) || $new_bucket_created) {
$backup_path = empty($posted_settings['backup_path']) ? '' : trailingslashit($posted_settings['backup_path']);
// Now try to write
$result = $storage->upload(array(
'BucketName' => $bucket_name,
'FileName' => $backup_path.$testfile,
'Body' => 'This is a test file resulting from pressing the "Test" button in UpdraftPlus, https://updraftplus.com. If it is still here afterwards, then something went wrong deleting it - you should delete it manually.',
));
if (is_object($result) && is_callable(array($result, 'getSize')) && $result->getSize() > 1) {
$result = true;
}
} elseif (!$new_bucket_created) {
printf(__("Failure: We could not successfully access or create such a bucket. Please check your access credentials, and if those are correct then try another bucket name (as another %s user may already have taken your name).", 'updraftplus'), 'Backblaze');
}
}
} catch (Exception $e) {
echo get_class($e).': '.$e->getMessage().' ('.$e->getCode().', '.get_class($e).') (line: '.$e->getLine().', file: '.$e->getFile().")\n";
}
return array('result' => $result, 'data' => $data);
}
/**
* Delete a temporary file use for a credentials test. Output can be echo-ed.
*
* @param String $testfile - the basename of the file to delete
* @param Array $posted_settings - the settings to use
*
* @return void
*/
protected function do_credentials_test_deletefile($testfile, $posted_settings) {
try {
$backup_path = empty($posted_settings['backup_path']) ? '' : trailingslashit($posted_settings['backup_path']);
$storage = $this->get_storage();
$storage->deleteFile(array(
'FileName' => $backup_path.$testfile,
'BucketName' => $posted_settings['bucket_name'],
));
} catch (Exception $e) {
echo __('Delete failed:', 'updraftplus').' '.$e->getMessage().' ('.$e->getCode().', '.get_class($e).') (line: '.$e->getLine().', file: '.$e->getFile().')';
}
}
/**
* Retrieve a list of supported features for this storage method
* This method should be over-ridden by methods supporting new
* features.
*
* @see UpdraftPlus_BackupModule::get_supported_features()
*
* @return Array - an array of supported features (any features not
* mentioned are assumed to not be supported)
*/
public function get_supported_features() {
// This options format is handled via only accessing options via $this->get_options()
return array('multi_options', 'config_templates', 'multi_storage', 'conditional_logic', 'multi_delete');
}
/**
* Retrieve default options for this remote storage module.
*
* @return Array - an array of options
*/
public function get_default_options() {
return array(
'account_id' => '',
'key' => '',
'bucket_name' => '',
'backup_path' => '',
'single_bucket_key_id' => '',
);
}
/**
* Perform any boot-strapping functions, and return a client instance
*
* @param Array $opts - instance options
* @param Boolean $connect - whether to also set up a connection (if supported by this method)
*
* @return UpdraftPlus_Backblaze_CurlClient|WP_Error - the storage object. It should also be stored as $this->storage.
*/
public function do_bootstrap($opts) {
$storage = $this->get_storage();
if (!empty($storage) && !is_wp_error($storage)) return $storage;
try {
if (!is_array($opts)) $opts = $this->get_options();
if (!class_exists('UpdraftPlus_Backblaze_CurlClient')) include_once UPDRAFTPLUS_DIR.'/includes/Backblaze/CurlClient.php';
if (empty($opts['account_id']) || empty($opts['key'])) return new WP_Error('no_settings', __('No settings were found', 'updraftplus').' (Backblaze)');
$backblaze_options = array(
'ssl_verify' => empty($opts['disableverify']),
'ssl_ca_certs' => empty($opts['useservercerts']) ? UPDRAFTPLUS_DIR.'/includes/cacert.pem' : false
);
$storage = new UpdraftPlus_Backblaze_CurlClient($opts['account_id'], $opts['key'], $opts['single_bucket_key_id'], $backblaze_options);
$this->set_storage($storage);
} catch (Exception $e) {
return new WP_Error('blob_service_failed', 'Error when attempting to setup Backblaze access (please check your credentials): '.$e->getMessage().' ('.$e->getCode().', '.get_class($e).') (line: '.$e->getLine().', file: '.$e->getFile().')');
}
return $storage;
}
/**
* Check whether options have been set up by the user, or not
*
* @param Array $opts - the potential options
*
* @return Boolean
*/
public function options_exist($opts) {
if (is_array($opts) && !empty($opts['account_id']) && !empty($opts['key'])) return true;
return false;
}
/**
* Get the pre configuration template
*
* @return String - the template
*/
public function get_pre_configuration_template() {
global $updraftplus_admin;
$classes = $this->get_css_classes(false);
?>
<tr class="<?php echo $classes . ' ' . 'backblaze_pre_config_container';?>">
<td colspan="2">
<img width="434" src="<?php echo UPDRAFTPLUS_URL;?>/images/backblaze.png"><br>
<?php $updraftplus_admin->curl_check('Backblaze B2', false, 'backblaze'); ?>
<p><a href="https://updraftplus.com/support/configuring-backblaze-cloud-storage-access-in-updraftplus/" target="_blank"><strong><?php echo sprintf(__('For help configuring %s, including screenshots, follow this link.', 'updraftplus'), 'Backblaze');?></strong></a></p>
</td>
</tr>
<?php
}
/**
* Get the configuration template
*
* @return String - the template, ready for substitutions to be carried out
*/
public function get_configuration_template() {
ob_start();
$classes = $this->get_css_classes();
?>
<tr class="<?php echo $classes;?>">
<th><?php echo _e('Master Application Key ID', 'updraftplus'); ?>:</th>
<td><input type="text" size="40" data-updraft_settings_test="account_id" <?php $this->output_settings_field_name_and_id('account_id');?> value="{{account_id}}"><br>
<em><?php echo sprintf(__('Get these settings from %s, or sign up %s.', 'updraftplus'), '<a aria-label="secure.backblaze.com/b2_buckets.htm" target="_blank" href="https://secure.backblaze.com/b2_buckets.htm">'.__('here', 'updraftplus').'</a>', '<a aria-label="www.backblaze.com/b2/" target="_blank" href="https://www.backblaze.com/b2/">'.__('here', 'updraftplus').'</a>');?></em></a><br>
</td>
</tr>
<tr class="<?php echo $classes;?>">
<th><?php _e('Application key', 'updraftplus'); ?>:</th>
<td><input type="<?php echo apply_filters('updraftplus_admin_secret_field_type', 'password'); ?>" size="40" data-updraft_settings_test="key" <?php $this->output_settings_field_name_and_id('key');?> value="{{key}}" /></td>
</tr>
<tr class="<?php echo $classes;?>">
<th><?php _e('Bucket application key ID', 'updraftplus'); ?>:</th>
<td><input title="<?php echo __('This is needed if, and only if, your application key was a bucket-specific application key (not a master key)', 'updraftplus');?>" type="text" size="40" data-updraft_settings_test="single_bucket_key_id" <?php $this->output_settings_field_name_and_id('single_bucket_key_id');?> value="{{single_bucket_key_id}}"><br>
<em><?php echo __('This is needed if, and only if, your application key was a bucket-specific application key (not a master key)', 'updraftplus');?></em></a><br>
</td>
</tr>
<tr class="<?php echo $classes;?>">
<th><?php _e('Backup path', 'updraftplus'); ?>:</th>
<td>/<input type="text" size="19" maxlength="50" placeholder="<?php _e('Bucket name', 'updraftplus');?>" data-updraft_settings_test="bucket_name" <?php $this->output_settings_field_name_and_id('bucket_name');?> value="{{bucket_name}}" />/<input type="text" size="19" maxlength="200" placeholder="<?php _e('some/path', 'updraftplus');?> " data-updraft_settings_test="backup_path" <?php $this->output_settings_field_name_and_id('backup_path');?> value="{{backup_path}}" /><br>
<em><?php echo '<a target="_blank" href="https://help.backblaze.com/hc/en-us/articles/217666908-What-you-need-to-know-about-B2-Bucket-names">'.__('There are limits upon which path-names are valid. Spaces are not allowed.', 'updraftplus').'</a>';?></em><br>
</td>
</tr>
<?php
echo $this->get_test_button_html('Backblaze');
return ob_get_clean();
}
/**
* Get bucket name list array for current storage instance
*
* @return array Which contains bucket names as element values
*/
protected function get_bucket_names_array() {
$bucket_names = array();
$storage = $this->get_storage();
$buckets = $storage->listBuckets();
if (is_array($buckets)) {
foreach ($buckets as $bucket) {
$bucket_names[] = $bucket->getName();
}
}
return $bucket_names;
}
/**
* Checks whether bucket name is valid as per backblaze standards
*
* @param string $bucket_name Backblaze bucket name
* @return boolean If bucket name is valid, it returns true. Otherwise false
*/
protected function is_valid_bucket_name($bucket_name) {
return preg_match('/^(?!b2-)[-0-9a-z]{6,50}$/i', $bucket_name);
}
}