Adds a new 'S3-Compatible Storage' provider that works with any
S3-API-compatible object storage service, including MinIO, Ceph,
Cloudflare R2, Backblaze B2, and others.
Changes:
- New provider class: classes/providers/storage/s3-compatible-provider.php
- Provider key: s3compatible
- Reads user-configured endpoint URL from settings
- Uses path-style URL access (required by most S3-compatible services)
- Supports credentials via AS3CF_S3COMPAT_ACCESS_KEY_ID /
AS3CF_S3COMPAT_SECRET_ACCESS_KEY wp-config.php constants
- Disables AWS-specific features (Block Public Access, Object Ownership)
- New provider SVG icons (s3compatible.svg, -link.svg, -round.svg)
- Registered provider in main plugin class with endpoint setting support
- Updated StorageProviderSubPage to show endpoint URL input for S3-compatible
- Built pro settings bundle with rollup (Svelte 4.2.19)
- Added package.json and updated rollup.config.mjs for pro-only builds
912 lines
23 KiB
PHP
912 lines
23 KiB
PHP
<?php
|
|
|
|
namespace DeliciousBrains\WP_Offload_Media\Providers\Storage;
|
|
|
|
use AS3CF_Plugin_Base;
|
|
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Core\Exception\GoogleException;
|
|
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Core\ServiceBuilder;
|
|
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Storage\Bucket;
|
|
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Storage\StorageClient;
|
|
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Storage\StorageObject;
|
|
use DeliciousBrains\WP_Offload_Media\Providers\Storage\Streams\GCP_GCS_Stream_Wrapper;
|
|
use Exception;
|
|
use WP_Error;
|
|
|
|
class GCP_Provider extends Storage_Provider {
|
|
|
|
/**
|
|
* @var ServiceBuilder
|
|
*/
|
|
private $cloud;
|
|
|
|
/**
|
|
* @var StorageClient
|
|
*/
|
|
private $storage;
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected static $provider_name = 'Google Cloud Platform';
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected static $provider_short_name = 'GCP';
|
|
|
|
/**
|
|
* Used in filters and settings.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected static $provider_key_name = 'gcp';
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected static $service_name = 'Google Cloud Storage';
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected static $service_short_name = 'GCS';
|
|
|
|
/**
|
|
* Used in filters and settings.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected static $service_key_name = 'gcs';
|
|
|
|
/**
|
|
* Optional override of "Provider Name" + "Service Name" for friendly name for service.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected static $provider_service_name = 'Google Cloud Storage';
|
|
|
|
/**
|
|
* The slug for the service's quick start guide doc.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected static $provider_service_quick_start_slug = 'google-cloud-storage-quick-start-guide';
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
protected static $use_server_roles_constants = array(
|
|
'AS3CF_GCP_USE_GCE_IAM_ROLE',
|
|
);
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
protected static $key_file_path_constants = array(
|
|
'AS3CF_GCP_KEY_FILE_PATH',
|
|
);
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
protected static $regions = array(
|
|
'asia' => 'Multi-Region (Asia)',
|
|
'eu' => 'Multi-Region (EU)',
|
|
'us' => 'Multi-Region (US)',
|
|
'us-central1' => 'North America (Iowa)',
|
|
'us-east1' => 'North America (South Carolina)',
|
|
'us-east4' => 'North America (Northern Virginia)',
|
|
'us-east5' => 'North America (Columbus)',
|
|
'us-west1' => 'North America (Oregon)',
|
|
'us-west2' => 'North America (Los Angeles)',
|
|
'us-west3' => 'North America (Salt Lake City)',
|
|
'us-west4' => 'North America (Las Vegas)',
|
|
'us-south1' => 'North America (Dallas)',
|
|
'northamerica-northeast1' => 'North America (Montréal)',
|
|
'northamerica-northeast2' => 'North America (Toronto)',
|
|
'southamerica-east1' => 'South America (São Paulo)',
|
|
'southamerica-west1' => 'South America (Santiago)',
|
|
'europe-central2' => 'Europe (Warsaw)',
|
|
'europe-north1' => 'Europe (Finland)',
|
|
'europe-west1' => 'Europe (Belgium)',
|
|
'europe-west2' => 'Europe (London)',
|
|
'europe-west3' => 'Europe (Frankfurt)',
|
|
'europe-west4' => 'Europe (Netherlands)',
|
|
'europe-west6' => 'Europe (Zürich)',
|
|
'europe-west8' => 'Europe (Milan)',
|
|
'europe-west9' => 'Europe (Paris)',
|
|
'europe-west10' => 'Europe (Berlin)',
|
|
'europe-west12' => 'Europe (Turin)',
|
|
'europe-southwest1' => 'Europe (Madrid)',
|
|
'asia-east1' => 'Asia (Taiwan)',
|
|
'asia-east2' => 'Asia (Hong Kong)',
|
|
'asia-northeast1' => 'Asia (Tokyo)',
|
|
'asia-northeast2' => 'Asia (Osaka)',
|
|
'asia-northeast3' => 'Asia (Seoul)',
|
|
'asia-southeast1' => 'Asia (Singapore)',
|
|
'asia-south1' => 'India (Mumbai)',
|
|
'asia-south2' => 'India (Dehli)',
|
|
'asia-southeast2' => 'Indonesia (Jakarta)',
|
|
'me-central1' => 'Middle East (Doha)',
|
|
'me-central2' => 'Middle East (Dammam, Saudi Arabia)',
|
|
'me-west1' => 'Middle East (Tel Aviv)',
|
|
'australia-southeast1' => 'Australia (Sydney)',
|
|
'australia-southeast2' => 'Australia (Melbourne)',
|
|
'africa-south1' => 'Africa (Johannesburg)',
|
|
'asia1' => 'Dual-Region (Tokyo/Osaka)',
|
|
'eur4' => 'Dual-Region (Finland/Netherlands)',
|
|
'eur5' => 'Dual-Region (Belgium/London)',
|
|
'eur7' => 'Dual-Region (London/Frankfurt)',
|
|
'eur8' => 'Dual-Region (Frankfurt/Zürich)',
|
|
'nam4' => 'Dual-Region (Iowa/South Carolina)',
|
|
);
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected static $default_region = 'us';
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $default_domain = 'storage.googleapis.com';
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $console_url = 'https://console.cloud.google.com/storage/browser/';
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $console_url_prefix_param = '/';
|
|
|
|
const PUBLIC_ACL = 'publicRead';
|
|
const PRIVATE_ACL = 'projectPrivate';
|
|
|
|
/**
|
|
* GCP_Provider constructor.
|
|
*
|
|
* @param AS3CF_Plugin_Base $as3cf
|
|
*/
|
|
public function __construct( AS3CF_Plugin_Base $as3cf ) {
|
|
parent::__construct( $as3cf );
|
|
|
|
// Autoloader.
|
|
require_once $as3cf->get_plugin_sdks_dir_path() . '/Gcp/autoload.php';
|
|
}
|
|
|
|
/**
|
|
* Returns default args array for the client.
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function default_client_args() {
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* Process the args before instantiating a new client for the provider's SDK.
|
|
*
|
|
* @param array $args
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function init_client_args( array $args ) {
|
|
return $args;
|
|
}
|
|
|
|
/**
|
|
* Instantiate a new client for the provider's SDK.
|
|
*
|
|
* @param array $args
|
|
*/
|
|
protected function init_client( array $args ) {
|
|
$this->cloud = new ServiceBuilder( $args );
|
|
}
|
|
|
|
/**
|
|
* Process the args before instantiating a new service specific client.
|
|
*
|
|
* @param array $args
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function init_service_client_args( array $args ) {
|
|
return $args;
|
|
}
|
|
|
|
/**
|
|
* Instantiate a new service specific client.
|
|
*
|
|
* @param array $args
|
|
*
|
|
* @return StorageClient
|
|
*/
|
|
protected function init_service_client( array $args ) {
|
|
$this->storage = $this->cloud->storage( $args );
|
|
|
|
return $this->storage;
|
|
}
|
|
|
|
/**
|
|
* Make sure region "slug" fits expected format.
|
|
*
|
|
* @param string $region
|
|
*
|
|
* @return string
|
|
*/
|
|
public function sanitize_region( $region ) {
|
|
if ( ! is_string( $region ) ) {
|
|
// Don't translate any region errors
|
|
return $region;
|
|
}
|
|
|
|
return strtolower( $region );
|
|
}
|
|
|
|
/**
|
|
* Create bucket.
|
|
*
|
|
* @param array $args
|
|
*
|
|
* @throws GoogleException
|
|
*/
|
|
public function create_bucket( array $args ) {
|
|
$name = '';
|
|
if ( ! empty( $args['Bucket'] ) ) {
|
|
$name = $args['Bucket'];
|
|
unset( $args['Bucket'] );
|
|
}
|
|
|
|
if ( ! empty( $args['LocationConstraint'] ) ) {
|
|
$args['location'] = $args['LocationConstraint'];
|
|
unset( $args['LocationConstraint'] );
|
|
}
|
|
|
|
$this->storage->createBucket( $name, $args );
|
|
}
|
|
|
|
/**
|
|
* Check whether bucket exists.
|
|
*
|
|
* @param string $bucket
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function does_bucket_exist( $bucket ) {
|
|
return $this->storage->bucket( $bucket )->exists();
|
|
}
|
|
|
|
/**
|
|
* Returns region for bucket.
|
|
*
|
|
* @param array $args
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_bucket_location( array $args ) {
|
|
$info = $this->storage->bucket( $args['Bucket'] )->info();
|
|
$region = empty( $info['location'] ) ? '' : $info['location'];
|
|
|
|
return $region;
|
|
}
|
|
|
|
/**
|
|
* List buckets.
|
|
*
|
|
* @param array $args
|
|
*
|
|
* @return array
|
|
* @throws GoogleException
|
|
*/
|
|
public function list_buckets( array $args = array() ) {
|
|
$result = array();
|
|
|
|
$buckets = $this->storage->buckets( $args );
|
|
|
|
if ( ! empty( $buckets ) ) {
|
|
/** @var Bucket $bucket */
|
|
foreach ( $buckets as $bucket ) {
|
|
$result['Buckets'][] = array(
|
|
'Name' => $bucket->name(),
|
|
);
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Check whether key exists in bucket.
|
|
*
|
|
* @param string $bucket
|
|
* @param string $key
|
|
* @param array $options
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function does_object_exist( $bucket, $key, array $options = array() ) {
|
|
return $this->storage->bucket( $bucket )->object( $key )->exists( $options );
|
|
}
|
|
|
|
/**
|
|
* Get public "canned" ACL string.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_public_acl() {
|
|
return static::PUBLIC_ACL;
|
|
}
|
|
|
|
/**
|
|
* Get private "canned" ACL string.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_private_acl() {
|
|
return static::PRIVATE_ACL;
|
|
}
|
|
|
|
/**
|
|
* Download object, destination specified in args.
|
|
*
|
|
* @see https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-s3-2006-03-01.html#getobject
|
|
* @see https://googleapis.github.io/google-cloud-php/#/docs/google-cloud/v0.90.0/storage/storageobject?method=downloadToFile
|
|
*
|
|
* @param array $args
|
|
*/
|
|
public function get_object( array $args ) {
|
|
$this->storage->bucket( $args['Bucket'] )->object( $args['Key'] )->downloadToFile( $args['SaveAs'] );
|
|
}
|
|
|
|
/**
|
|
* Get object's URL.
|
|
*
|
|
* @param string $bucket
|
|
* @param string $key
|
|
* @param int $timestamp
|
|
* @param array $args
|
|
*
|
|
* @return string
|
|
*/
|
|
public function get_object_url( $bucket, $key, $timestamp, array $args = array() ) {
|
|
if ( empty( $timestamp ) || ! is_int( $timestamp ) || $timestamp < 0 ) {
|
|
$info = $this->storage->bucket( $bucket )->object( $key )->info();
|
|
$link = empty( $info['selfLink'] ) ? '' : $info['selfLink'];
|
|
|
|
return $link;
|
|
} else {
|
|
$options = array();
|
|
|
|
if ( ! empty( $args['BaseURL'] ) ) {
|
|
$options['cname'] = $args['BaseURL'];
|
|
}
|
|
|
|
return $this->storage->bucket( $bucket )->object( $key )->signedUrl( $timestamp, $options );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List objects.
|
|
*
|
|
* @param array $args
|
|
*
|
|
* @return array
|
|
*/
|
|
public function list_objects( array $args = array() ) {
|
|
$result = array();
|
|
|
|
$objects = $this->storage->bucket( $args['Bucket'] )->objects( $args['Prefix'] );
|
|
|
|
if ( ! empty( $objects ) ) {
|
|
/** @var StorageObject $object */
|
|
foreach ( $objects as $object ) {
|
|
$info = $object->info();
|
|
$result['Contents'][] = array(
|
|
'Key' => $object->name(),
|
|
'Size' => $info['size'],
|
|
);
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Update the ACL for an object.
|
|
*
|
|
* @param array $args
|
|
*
|
|
* @throws Exception
|
|
*/
|
|
public function update_object_acl( array $args ) {
|
|
if ( empty( $args['ACL'] ) ) {
|
|
throw new Exception( __METHOD__ . ' called without "ACL" arg.' );
|
|
}
|
|
|
|
$this->storage->bucket( $args['Bucket'] )->object( $args['Key'] )->update( array( 'predefinedAcl' => $args['ACL'] ) );
|
|
}
|
|
|
|
/**
|
|
* Update the ACL for multiple objects.
|
|
*
|
|
* @param array $items
|
|
*
|
|
* @return array Failures with elements Key and Message
|
|
*/
|
|
public function update_object_acls( array $items ) {
|
|
$failures = array();
|
|
|
|
// Unfortunately the GCP PHP SDK does not have batch operations.
|
|
foreach ( $items as $item ) {
|
|
try {
|
|
$this->update_object_acl( $item );
|
|
} catch ( Exception $e ) {
|
|
$failures[] = array(
|
|
'Key' => $item['Key'],
|
|
'Message' => $e->getMessage(),
|
|
);
|
|
}
|
|
}
|
|
|
|
return $failures;
|
|
}
|
|
|
|
/**
|
|
* Upload file to bucket.
|
|
*
|
|
* @param array $args
|
|
*
|
|
* @throws Exception
|
|
*/
|
|
public function upload_object( array $args ) {
|
|
if ( ! empty( $args['SourceFile'] ) ) {
|
|
$file = fopen( $args['SourceFile'], 'r' );
|
|
} elseif ( ! empty( $args['Body'] ) ) {
|
|
$file = $args['Body'];
|
|
} else {
|
|
throw new Exception( __METHOD__ . ' called without either "SourceFile" or "Body" arg.' );
|
|
}
|
|
|
|
$options = array(
|
|
'name' => $args['Key'],
|
|
);
|
|
|
|
if ( ! empty( $args['ACL'] ) ) {
|
|
$options['predefinedAcl'] = $args['ACL'];
|
|
}
|
|
|
|
if ( ! empty( $args['ContentType'] ) ) {
|
|
$options['metadata']['contentType'] = $args['ContentType'];
|
|
}
|
|
|
|
if ( ! empty( $args['CacheControl'] ) ) {
|
|
$options['metadata']['cacheControl'] = $args['CacheControl'];
|
|
}
|
|
|
|
// TODO: Potentially strip out known keys from $args and then put rest in $options['metadata']['metadata'].
|
|
|
|
$object = $this->storage->bucket( $args['Bucket'] )->upload( $file, $options ); // phpcs:ignore
|
|
}
|
|
|
|
/**
|
|
* Delete object from bucket.
|
|
*
|
|
* @param array $args
|
|
*/
|
|
public function delete_object( array $args ) {
|
|
$this->storage->bucket( $args['Bucket'] )->object( $args['Key'] )->delete();
|
|
}
|
|
|
|
/**
|
|
* Delete multiple objects from bucket.
|
|
*
|
|
* @param array $args
|
|
*/
|
|
public function delete_objects( array $args ) {
|
|
if ( isset( $args['Delete']['Objects'] ) ) {
|
|
$keys = $args['Delete']['Objects'];
|
|
} elseif ( isset( $args['Objects'] ) ) {
|
|
$keys = $args['Objects'];
|
|
}
|
|
|
|
if ( ! empty( $keys ) ) {
|
|
$bucket = $this->storage->bucket( $args['Bucket'] );
|
|
|
|
// Unfortunately the GCP PHP SDK does not have batch operations.
|
|
foreach ( $keys as $key ) {
|
|
$bucket->object( $key['Key'] )->delete();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns arrays of found keys for given bucket and prefix locations, retaining given array's integer based index.
|
|
*
|
|
* @param array $locations Array with attachment ID as key and Bucket and Prefix in an associative array as values.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function list_keys( array $locations ) {
|
|
$keys = array();
|
|
|
|
$results = array_map( function ( $location ) {
|
|
return $this->storage->bucket( $location['Bucket'] )->objects( array(
|
|
'prefix' => $location['Prefix'],
|
|
'fields' => 'items/name',
|
|
) );
|
|
}, $locations );
|
|
|
|
foreach ( $results as $attachment_id => $objects ) {
|
|
/** @var StorageObject $object */
|
|
foreach ( $objects as $object ) {
|
|
$keys[ $attachment_id ][] = $object->name();
|
|
}
|
|
}
|
|
|
|
return $keys;
|
|
}
|
|
|
|
/**
|
|
* Copies objects into current bucket from another bucket hosted with provider.
|
|
*
|
|
* @param array $items
|
|
*
|
|
* @return array Failures with elements Key and Message
|
|
*/
|
|
public function copy_objects( array $items ) {
|
|
$failures = array();
|
|
|
|
// Unfortunately the GCP PHP SDK does not have batch operations.
|
|
foreach ( $items as $item ) {
|
|
list( $bucket, $key ) = explode( '/', urldecode( $item['CopySource'] ), 2 );
|
|
|
|
$options = array(
|
|
'name' => $item['Key'],
|
|
);
|
|
|
|
if ( ! empty( $item['ACL'] ) ) {
|
|
$options['predefinedAcl'] = $item['ACL'];
|
|
}
|
|
|
|
try {
|
|
$this->storage->bucket( $bucket )->object( $key )->copy( $item['Bucket'], $options );
|
|
} catch ( Exception $e ) {
|
|
$failures[] = array(
|
|
'Key' => $item['Key'],
|
|
'Message' => $e->getMessage(),
|
|
);
|
|
}
|
|
}
|
|
|
|
return $failures;
|
|
}
|
|
|
|
/**
|
|
* Generate the stream wrapper protocol
|
|
*
|
|
* @param string $region
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function get_stream_wrapper_protocol( $region ) {
|
|
$protocol = 'gs';
|
|
|
|
// TODO: Determine whether same protocol for all regions is ok.
|
|
// Assumption not as each may have client instance, hence keeping this for time being.
|
|
$protocol .= str_replace( '-', '', $region );
|
|
|
|
return $protocol;
|
|
}
|
|
|
|
/**
|
|
* Register a stream wrapper for specific region.
|
|
*
|
|
* @param string $region
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function register_stream_wrapper( $region ) {
|
|
$protocol = $this->get_stream_wrapper_protocol( $region );
|
|
|
|
// Register the region specific stream wrapper to be used by plugins
|
|
GCP_GCS_Stream_Wrapper::register( $this->storage, $protocol );
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check that a bucket and key can be written to.
|
|
*
|
|
* @param string $bucket
|
|
* @param string $key
|
|
* @param string $file_contents
|
|
*
|
|
* @return bool|string Error message on unexpected exception
|
|
*/
|
|
public function can_write( $bucket, $key, $file_contents ) {
|
|
try {
|
|
// Attempt to create the test file.
|
|
$this->upload_object(
|
|
static::filter_object_meta(
|
|
array(
|
|
'Bucket' => $bucket,
|
|
'Key' => $key,
|
|
'Body' => $file_contents,
|
|
)
|
|
)
|
|
);
|
|
|
|
// delete it straight away if created
|
|
$this->delete_object( array(
|
|
'Bucket' => $bucket,
|
|
'Key' => $key,
|
|
) );
|
|
|
|
return true;
|
|
} catch ( Exception $e ) {
|
|
// If we encounter an error that isn't from Google, throw that error.
|
|
if ( ! $e instanceof GoogleException ) {
|
|
return $e->getMessage();
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get the region specific prefix for raw URL
|
|
*
|
|
* @param string $region
|
|
* @param null|int $expires
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function url_prefix( $region = '', $expires = null ) {
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Get the url domain for the files
|
|
*
|
|
* @param string $domain Likely prefixed with region
|
|
* @param string $bucket
|
|
* @param string $region
|
|
* @param int $expires
|
|
* @param array $args Allows you to specify custom URL settings
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function url_domain( $domain, $bucket, $region = '', $expires = null, $args = array() ) {
|
|
if (
|
|
apply_filters(
|
|
'as3cf_' . static::get_provider_key_name() . '_' . static::get_service_key_name() . '_bucket_in_path',
|
|
false !== strpos( $bucket, '.' )
|
|
)
|
|
) {
|
|
$domain = $domain . '/' . $bucket;
|
|
} else {
|
|
// TODO: Is this mode allowed for GCS native URLs?
|
|
$domain = $bucket . '.' . $domain;
|
|
}
|
|
|
|
return $domain;
|
|
}
|
|
|
|
/**
|
|
* Get the suffix param to append to the link to the provider's console.
|
|
*
|
|
* @param string $bucket
|
|
* @param string $prefix
|
|
* @param string $region
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function get_console_url_suffix_param(
|
|
string $bucket = '',
|
|
string $prefix = '',
|
|
string $region = ''
|
|
): string {
|
|
if ( ! empty( $this->get_project_id() ) ) {
|
|
return '?project=' . $this->get_project_id();
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Get the Project ID for the current client.
|
|
*
|
|
* @return string|null
|
|
*/
|
|
private function get_project_id() {
|
|
static $project_id = null;
|
|
|
|
// If not already grabbed, get project id from key file data but only if client properly instantiated.
|
|
if ( null === $project_id && ! empty( $this->storage ) && $this->use_key_file() ) {
|
|
$key_file_path = $this->get_key_file_path();
|
|
|
|
if ( ! empty( $key_file_path ) && file_exists( $key_file_path ) ) {
|
|
$key_file_contents = json_decode( file_get_contents( $key_file_path ), true );
|
|
|
|
if ( ! empty( $key_file_contents['project_id'] ) ) {
|
|
$project_id = $key_file_contents['project_id'];
|
|
|
|
return $project_id;
|
|
}
|
|
}
|
|
|
|
$key_file_contents = $this->get_key_file();
|
|
|
|
if ( is_array( $key_file_contents ) && ! empty( $key_file_contents['project_id'] ) ) {
|
|
$project_id = $key_file_contents['project_id'];
|
|
|
|
return $project_id;
|
|
}
|
|
}
|
|
|
|
return $project_id;
|
|
}
|
|
|
|
/**
|
|
* Read key file contents from path and convert it to the appropriate format for this provider.
|
|
*
|
|
* @param string $path
|
|
*
|
|
* @return mixed
|
|
*/
|
|
protected function get_key_file_path_contents( string $path ) {
|
|
$notice_id = 'validate-key-file-path';
|
|
$notice_args = array(
|
|
'type' => 'error',
|
|
'only_show_in_settings' => true,
|
|
'only_show_on_tab' => 'media',
|
|
'hide_on_parent' => true,
|
|
'custom_id' => $notice_id,
|
|
);
|
|
|
|
$content = json_decode( file_get_contents( $path ), true );
|
|
|
|
if ( empty( $content ) ) {
|
|
$this->as3cf->notices->add_notice(
|
|
__( 'Media cannot be offloaded due to invalid JSON in the key file.', 'amazon-s3-and-cloudfront' ),
|
|
$notice_args
|
|
);
|
|
|
|
return false;
|
|
}
|
|
|
|
return $content;
|
|
}
|
|
|
|
/**
|
|
* Google specific validation of the key file contents.
|
|
*
|
|
* @param array $key_file_content
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function validate_key_file_content( $key_file_content ): bool {
|
|
$notice_id = 'validate-key-file-content';
|
|
$this->as3cf->notices->remove_notice_by_id( $notice_id );
|
|
|
|
$notice_args = array(
|
|
'type' => 'error',
|
|
'only_show_in_settings' => true,
|
|
'only_show_on_tab' => 'media',
|
|
'hide_on_parent' => true,
|
|
'custom_id' => $notice_id,
|
|
);
|
|
|
|
if ( ! isset( $key_file_content['project_id'] ) ) {
|
|
$this->as3cf->notices->add_notice(
|
|
sprintf(
|
|
__(
|
|
'Media cannot be offloaded due to a missing <code>project_id</code> field which may be the result of an old or obsolete key file. <a href="%1$s" target="_blank">Create a new key file</a>',
|
|
'amazon-s3-and-cloudfront'
|
|
),
|
|
static::get_provider_service_quick_start_url() . '#service-account-key-file'
|
|
),
|
|
$notice_args
|
|
);
|
|
|
|
return false;
|
|
}
|
|
|
|
if ( ! isset( $key_file_content['private_key'] ) ) {
|
|
$this->as3cf->notices->add_notice(
|
|
sprintf(
|
|
__(
|
|
'Media cannot be offloaded due to a missing <code>private_key</code> field in the key file. <a href="%1$s" target="_blank"">Create a new key file</a>',
|
|
'amazon-s3-and-cloudfront'
|
|
),
|
|
static::get_provider_service_quick_start_url() . '#service-account-key-file'
|
|
),
|
|
$notice_args
|
|
);
|
|
|
|
return false;
|
|
}
|
|
|
|
if ( ! isset( $key_file_content['type'] ) ) {
|
|
$this->as3cf->notices->add_notice(
|
|
sprintf(
|
|
__(
|
|
'Media cannot be offloaded due to a missing <code>type</code> field in the key file. <a href="%1$s" target="_blank">Create a new key file</a>',
|
|
'amazon-s3-and-cloudfront'
|
|
),
|
|
static::get_provider_service_quick_start_url() . '#service-account-key-file'
|
|
),
|
|
$notice_args
|
|
);
|
|
|
|
return false;
|
|
}
|
|
|
|
if ( ! isset( $key_file_content['client_email'] ) ) {
|
|
$this->as3cf->notices->add_notice(
|
|
sprintf(
|
|
__(
|
|
'Media cannot be offloaded due to a missing <code>client_email</code> field in the key file. <a href="%1$s" target="_blank">Create a new key file</a>',
|
|
'amazon-s3-and-cloudfront'
|
|
),
|
|
static::get_provider_service_quick_start_url() . '#service-account-key-file'
|
|
),
|
|
$notice_args
|
|
);
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Prepare the bucket error.
|
|
*
|
|
* @param WP_Error $object
|
|
* @param bool $single Are we dealing with a single bucket?
|
|
*
|
|
* @return string
|
|
*/
|
|
public function prepare_bucket_error( WP_Error $object, bool $single = true ): string {
|
|
if ( false !== strpos( $object->get_error_message(), "OpenSSL unable to sign" ) ) {
|
|
return sprintf(
|
|
__(
|
|
'Media cannot be offloaded due to an invalid OpenSSL Private Key. <a href="%1$s" target="_blank">Update the key file</a>',
|
|
'amazon-s3-and-cloudfront'
|
|
),
|
|
static::get_provider_service_quick_start_url() . '#service-account-key-file'
|
|
);
|
|
}
|
|
|
|
// This may be a JSON error message from Google.
|
|
$message = json_decode( $object->get_error_message() );
|
|
if ( ! is_null( $message ) ) {
|
|
if ( isset( $message->error ) && 'invalid_grant' === $message->error ) {
|
|
return sprintf(
|
|
__(
|
|
'Media cannot be offloaded using the provided service account. <a href="%1$s" target="_blank">Read more</a>',
|
|
'amazon-s3-and-cloudfront'
|
|
),
|
|
static::get_provider_service_quick_start_url() . '#service-account-key-file'
|
|
);
|
|
}
|
|
|
|
if ( isset( $message->error->code ) && 404 === $message->error->code ) {
|
|
return sprintf(
|
|
__(
|
|
'Media cannot be offloaded because a bucket with the configured name does not exist. <a href="%1$s">Enter a different bucket</a>',
|
|
'amazon-s3-and-cloudfront'
|
|
),
|
|
'#/storage/bucket'
|
|
);
|
|
}
|
|
}
|
|
|
|
// Fallback to generic error parsing.
|
|
return parent::prepare_bucket_error( $object, $single );
|
|
}
|
|
}
|