feat: add S3-compatible storage provider (MinIO, Ceph, R2, etc.)

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
This commit is contained in:
2026-03-03 12:30:18 +01:00
commit 3248cbb029
2086 changed files with 359427 additions and 0 deletions

View File

@@ -0,0 +1,200 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Providers\Delivery;
use AS3CF_Plugin_Base;
class AWS_CloudFront extends Delivery_Provider {
/**
* Which storage providers does the delivery provider support, empty means all.
*
* @var array
*/
protected static $supported_storage_providers = array(
'aws',
);
/**
* @var string
*/
protected static $provider_name = 'Amazon Web Services';
/**
* @var string
*/
protected static $provider_short_name = 'AWS';
/**
* Used in filters and settings.
*
* @var string
*/
protected static $provider_key_name = 'aws';
/**
* @var string
*/
protected static $service_name = 'CloudFront';
/**
* @var string
*/
protected static $service_short_name = 'CloudFront';
/**
* Used in filters and settings.
*
* @var string
*/
protected static $service_key_name = 'cloudfront';
/**
* Optional override of "Provider Name" + "Service Name" for friendly name for service.
*
* @var string
*/
protected static $provider_service_name = 'Amazon CloudFront';
/**
* The slug for the service's quick start guide doc.
*
* @var string
*/
protected static $provider_service_quick_start_slug = 'cloudfront-setup';
/**
* @var string
*/
protected $default_domain = 'cloudfront.net';
/**
* @var string
*/
protected $console_url = 'https://console.aws.amazon.com/cloudfront/home';
/**
* @var bool
*/
protected static $block_public_access_supported = true;
/**
* @var bool
*/
protected static $object_ownership_supported = true;
/**
* AWS_CloudFront 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() . '/Aws3/aws-autoloader.php';
}
/**
* @inheritDoc
*/
public static function signed_urls_support_desc() {
global $as3cf;
return sprintf(
__( 'Private Media Supported with <a href="%s" target="_blank">upgrade</a>', 'amazon-s3-and-cloudfront' ),
$as3cf::dbrains_url( '/wp-offload-media/upgrade/', array(
'utm_campaign' => 'WP+Offload+S3',
) )
);
}
/**
* Description for when Block All Public Access is enabled and Delivery Provider supports it.
*
* @return string
*/
public static function get_block_public_access_enabled_supported_desc() {
global $as3cf;
$mesg = __( 'Since you\'re using Amazon CloudFront for delivery we recommend you keep Block All Public Access enabled.', 'amazon-s3-and-cloudfront' );
$mesg .= '&nbsp;';
$mesg .= $as3cf::settings_more_info_link( 'bucket', '', 'change+bucket+access' );
return $mesg;
}
/**
* Description for when Block All Public Access is disabled and Delivery Provider supports it.
*
* @return string
*/
public static function get_block_public_access_disabled_supported_desc() {
global $as3cf;
$mesg = __( 'Since you\'re using Amazon CloudFront for delivery we recommend you enable Block All Public Access once you have set up the required Origin Access Identity and bucket policy.', 'amazon-s3-and-cloudfront' );
$mesg .= '&nbsp;';
$mesg .= $as3cf::settings_more_info_link( 'bucket', '', 'change+bucket+access' );
return $mesg;
}
/**
* Prompt text to confirm that everything is in place to enable Block All Public Access.
*
* @return string
*/
public static function get_block_public_access_confirm_setup_prompt() {
global $as3cf;
$bucket_settings_doc = $as3cf::dbrains_url(
'/wp-offload-media/doc/settings/',
array( 'utm_campaign' => 'support+docs', 'utm_content' => 'change+bucket+access' ),
'bucket'
);
return sprintf(
__( 'I have set up the required <a href="%1$s">Origin Access Identity and bucket policy</a>', 'amazon-s3-and-cloudfront' ),
$bucket_settings_doc
);
}
/**
* Description for when Object Ownership is enforced and Delivery Provider supports it.
*
* @return string
*/
public static function get_object_ownership_enforced_supported_desc(): string {
global $as3cf;
$mesg = __( 'Since you\'re using Amazon CloudFront for delivery we recommend you keep Object Ownership enforced.', 'amazon-s3-and-cloudfront' );
$mesg .= '&nbsp;';
$mesg .= $as3cf::settings_more_info_link( 'bucket', '', 'change+bucket+access' );
return $mesg;
}
/**
* Description for when Object Ownership is not enforced and Delivery Provider supports it.
*
* @return string
*/
public static function get_object_ownership_not_enforced_supported_desc(): string {
global $as3cf;
$mesg = __( 'Since you\'re using Amazon CloudFront for delivery we recommend you enforce Object Ownership once you have set up the required Origin Access Identity and bucket policy.', 'amazon-s3-and-cloudfront' );
$mesg .= '&nbsp;';
$mesg .= $as3cf::settings_more_info_link( 'bucket', '', 'change+bucket+access' );
return $mesg;
}
/**
* Title to be shown for provider's console link.
*
* @return string
*/
public static function get_console_title(): string {
return _x( 'CloudFront Distributions', 'Provider console link text', 'amazon-s3-and-cloudfront' );
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Providers\Delivery;
class Cloudflare extends Delivery_Provider {
/**
* Which storage providers does the delivery provider support, empty means all.
*
* @var array
*/
protected static $supported_storage_providers = array(
// TODO: Add 'do' after testing and documenting.
'aws',
);
/**
* @var string
*/
protected static $provider_name = 'Cloudflare';
/**
* @var string
*/
protected static $provider_short_name = 'Cloudflare';
/**
* Used in filters and settings.
*
* @var string
*/
protected static $provider_key_name = 'cloudflare';
/**
* @var string
*/
protected static $service_name = 'CDN';
/**
* @var string
*/
protected static $service_short_name = 'CDN';
/**
* Used in filters and settings.
*
* @var string
*/
protected static $service_key_name = 'cdn';
/**
* Optional override of "Provider Name" + "Service Name" for friendly name for service.
*
* @var string
*/
protected static $provider_service_name = 'Cloudflare';
/**
* @var string
*/
protected static $provider_service_quick_start_slug = 'cloudflare-setup';
/**
* @var string
*/
protected $console_url = 'https://dash.cloudflare.com';
/**
* Title to be shown for provider's console link.
*
* @return string
*/
public static function get_console_title(): string {
return _x( 'Cloudflare Dashboard', 'Provider console link text', 'amazon-s3-and-cloudfront' );
}
}

View File

@@ -0,0 +1,927 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Providers\Delivery;
use AS3CF_Utils;
use DeliciousBrains\WP_Offload_Media\Settings_Validator_Trait;
use DeliciousBrains\WP_Offload_Media\Items\Item;
use DeliciousBrains\WP_Offload_Media\Providers\Provider;
use DeliciousBrains\WP_Offload_Media\Settings\Delivery_Check;
use DeliciousBrains\WP_Offload_Media\Settings\Domain_Check;
use DeliciousBrains\WP_Offload_Media\Settings\Validator_Interface;
use Exception;
use WP_Error as AS3CF_Result;
abstract class Delivery_Provider extends Provider implements Validator_Interface {
use Settings_Validator_Trait;
const VALIDATOR_KEY = 'delivery';
/**
* @var string
*/
protected static $provider_service_name_setting_name = 'delivery-provider-service-name';
/**
* @var string
*/
protected $console_url_prefix_param = '/';
/**
* @var bool
*/
protected static $delivery_domain_allowed = true;
/**
* Which storage providers does the delivery provider support, empty means all.
*
* @var array
*/
protected static $supported_storage_providers = array();
/**
* @var string
*/
protected static $signed_urls_key_id_setting_name = 'signed-urls-key-id';
/**
* @var string
*/
protected static $signed_urls_key_file_path_setting_name = 'signed-urls-key-file-path';
/**
* @var string
*/
protected static $signed_urls_object_prefix_setting_name = 'signed-urls-object-prefix';
/**
* If left empty, private signing key file not allowed.
*
* @var array
*/
protected static $signed_urls_key_id_constants = array();
/**
* If left empty, private signing key file not allowed.
*
* @var array
*/
protected static $signed_urls_key_file_path_constants = array();
/**
* If left empty, private signing key file not allowed.
*
* @var array
*/
protected static $signed_urls_object_prefix_constants = array();
/**
* @var int
*/
private $validator_priority = 10;
/**
* A description for the Rewrite Media URLs setting.
*
* @return string
*/
public static function get_rewrite_media_urls_desc(): string {
global $as3cf;
$mesg = sprintf(
_x( 'Serves offloaded media files by rewriting local URLs so that they point to %s.', 'Setting description', 'amazon-s3-and-cloudfront' ),
static::get_provider_service_name()
);
$mesg .= ' ' . $as3cf::settings_more_info_link( 'serve-from-s3', 'How URL rewriting works' );
return $mesg;
}
/**
* Does the delivery provider allow for setting a custom delivery domain?
*
* @return bool
*/
public static function delivery_domain_allowed() {
return static::$delivery_domain_allowed;
}
/**
* A description for the delivery domain settings.
*
* @return string
*/
public static function get_delivery_domain_desc(): string {
return sprintf(
__( 'Serves media from a custom domain that has been pointed to %1$s. <a href="%2$s" target="_blank">How to set a custom domain name</a>', 'amazon-s3-and-cloudfront' ),
static::get_provider_service_name(),
static::get_provider_service_quick_start_url() . '#create-cname'
);
}
/**
* Returns array of supported storage provider key names, empty array means all supported.
*
* @return array
*/
public static function get_supported_storage_providers() {
return static::$supported_storage_providers;
}
/**
* Does provider support given storage provider?
*
* @param string $storage_provider_key
*
* @return bool
*/
public static function supports_storage( $storage_provider_key ) {
if ( empty( static::$supported_storage_providers ) || in_array( $storage_provider_key, static::$supported_storage_providers ) ) {
return true;
}
return false;
}
/**
* Title used in various places for enabling Signed URLs.
*
* @return string
*/
public static function signed_urls_option_name() {
return __( 'Serve Private Media from Delivery Provider', 'amazon-s3-and-cloudfront' );
}
/**
* Description used in various places for enabling Signed URLs.
*
* @return string
*/
public static function signed_urls_option_description() {
return '';
}
/**
* Title used in various places for the Signed URLs Key ID.
*
* @return string
*/
public static function signed_urls_key_id_name() {
return __( 'Signing Key ID', 'amazon-s3-and-cloudfront' );
}
/**
* Description used in various places for the Signed URLs Key ID.
*
* @return string
*/
public static function signed_urls_key_id_description() {
return '';
}
/**
* Title used in various places for the Signed URLs Key File Path.
*
* @return string
*/
public static function signed_urls_key_file_path_name() {
return __( 'Signing Key File Path', 'amazon-s3-and-cloudfront' );
}
/**
* Description used in various places for the Signed URLs Key File Path.
*
* @return string
*/
public static function signed_urls_key_file_path_description() {
return '';
}
/**
* Title used in various places for the Signed URLs Private Object Prefix.
*
* @return string
*/
public static function signed_urls_object_prefix_name() {
return __( 'Private Bucket Path', 'amazon-s3-and-cloudfront' );
}
/**
* Description used in various places for the Signed URLs Private Object Prefix.
*
* @return string
*/
public static function signed_urls_object_prefix_description() {
return '';
}
/**
* Description used in settings notice when all tests pass.
*
* @param array $recommendations Array of hints/recommendation to add to the success message.
*
* @return string
*/
protected static function delivery_tests_pass_desc( array $recommendations = array() ): string {
$message = __( 'Delivery provider is successfully connected and serving offloaded media.', 'amazon-s3-and-cloudfront' );
if ( ! empty( $recommendations ) ) {
$message = __( 'Delivery settings validated.', 'amazon-s3-and-cloudfront' );
$message .= '<br><br>';
$message .= join( '<br><br>', $recommendations );
}
return $message;
}
/**
* Is the provider able to use a private signing key file?
*
* @return bool
*/
public static function use_signed_urls_key_file_allowed() {
return ! empty( static::$signed_urls_key_id_constants ) && ! empty( static::$signed_urls_key_file_path_constants ) && ! empty( static::$signed_urls_object_prefix_constants );
}
/**
* If private signing key file allowed, returns first (preferred) key id constant that should be defined, otherwise blank.
*
* @return string
*/
public static function preferred_signed_urls_key_id_constant() {
if ( static::use_signed_urls_key_file_allowed() ) {
return static::$signed_urls_key_id_constants[0];
} else {
return '';
}
}
/**
* If private signing key file allowed, returns first (preferred) key file path constant that should be defined, otherwise blank.
*
* @return string
*/
public static function preferred_signed_urls_key_file_path_constant() {
if ( static::use_signed_urls_key_file_allowed() ) {
return static::$signed_urls_key_file_path_constants[0];
} else {
return '';
}
}
/**
* If private signing key file allowed, returns first (preferred) object prefix constant that should be defined, otherwise blank.
*
* @return string
*/
public static function preferred_signed_urls_object_prefix_constant() {
if ( static::use_signed_urls_key_file_allowed() ) {
return static::$signed_urls_object_prefix_constants[0];
} else {
return '';
}
}
/**
* Check if private signing key id and file path set.
*
* @return bool
*/
public function use_signed_urls_key_file() {
if ( ! static::use_signed_urls_key_file_allowed() ) {
return false;
}
return $this->get_signed_urls_key_id() && $this->get_signed_urls_key_file_path() && $this->get_signed_urls_object_prefix();
}
/**
* Get the private signing key id from a constant or the settings.
*
* Falls back to settings if constant is not defined.
*
* @return string
*/
public function get_signed_urls_key_id() {
if ( static::is_signed_urls_key_id_constant_defined() ) {
$constant = static::signed_urls_key_id_constant();
return $constant ? constant( $constant ) : '';
}
return $this->as3cf->get_core_setting( static::$signed_urls_key_id_setting_name );
}
/**
* Get the private signing key file path from a constant or the settings.
*
* Falls back to settings if constant is not defined.
*
* @return string|bool
*/
public function get_signed_urls_key_file_path() {
if ( static::is_signed_urls_key_file_path_constant_defined() ) {
$constant = static::signed_urls_key_file_path_constant();
if ( $constant ) {
return $this->validate_signed_urls_key_file_path( constant( $constant ) );
} else {
// Constant defined but value is not a non-empty string.
return false;
}
}
return $this->validate_signed_urls_key_file_path( $this->as3cf->get_core_setting( static::$signed_urls_key_file_path_setting_name, false ) );
}
/**
* Get the private signing object prefix from a constant or the settings.
*
* Falls back to settings if constant is not defined.
*
* @return string
*/
public function get_signed_urls_object_prefix() {
if ( static::is_signed_urls_object_prefix_constant_defined() ) {
$constant = static::signed_urls_object_prefix_constant();
return $constant ? constant( $constant ) : '';
}
return $this->as3cf->get_core_setting( static::$signed_urls_object_prefix_setting_name );
}
/**
* Validate a private signing key file path to ensure it exists, is readable, and contains something.
*
* @param string $signed_urls_key_file_path
*
* @return bool|string
*/
public function validate_signed_urls_key_file_path( $signed_urls_key_file_path ) {
$notice_id = ( $this->as3cf->saving_settings() ? 'temp-' : '' ) . 'validate-signed-urls-key-file-path';
$this->as3cf->notices->remove_notice_by_id( $notice_id );
$notice_settings = array(
'type' => 'error',
'only_show_in_settings' => true,
'only_show_on_tab' => 'media',
'custom_id' => $notice_id,
'hide_on_parent' => ! $this->as3cf->saving_settings(),
);
if ( empty( $signed_urls_key_file_path ) ) {
return false;
}
if ( ! file_exists( $signed_urls_key_file_path ) ) {
$this->as3cf->notices->add_notice( __( 'Given Signing Key File Path is invalid or could not be accessed.', 'amazon-s3-and-cloudfront' ), $notice_settings );
return false;
}
try {
$value = file_get_contents( $signed_urls_key_file_path );
// An exception isn't always thrown, so check value instead.
if ( empty( $value ) ) {
$this->as3cf->notices->add_notice( __( 'Could not read Signing Key File Path\'s contents.', 'amazon-s3-and-cloudfront' ), $notice_settings );
return false;
}
} catch ( Exception $e ) {
$this->as3cf->notices->add_notice( __( 'Could not read Signing Key File Path\'s contents.', 'amazon-s3-and-cloudfront' ), $notice_settings );
return false;
}
// File exists and has contents.
return $signed_urls_key_file_path;
}
/**
* Check if private signing key id constant is defined.
*
* @return bool
*/
public static function is_signed_urls_key_id_constant_defined() {
$constant = static::signed_urls_key_id_constant();
return $constant && constant( $constant );
}
/**
* Check if private signing key file path constant is defined, for speed, does not check validity of file path.
*
* @return bool
*/
public static function is_signed_urls_key_file_path_constant_defined() {
$constant = static::signed_urls_key_file_path_constant();
return $constant && constant( $constant );
}
/**
* Check if private signing object prefix constant is defined.
*
* @return bool
*/
public static function is_signed_urls_object_prefix_constant_defined() {
$constant = static::signed_urls_object_prefix_constant();
return $constant && constant( $constant );
}
/**
* Get the constant used to define the private signing key id.
*
* @return string|false Constant name if defined, otherwise false
*/
public static function signed_urls_key_id_constant() {
return AS3CF_Utils::get_first_defined_constant( static::$signed_urls_key_id_constants );
}
/**
* Get the constant used to define the private signing key file path.
*
* @return string|false Constant name if defined, otherwise false
*/
public static function signed_urls_key_file_path_constant() {
return AS3CF_Utils::get_first_defined_constant( static::$signed_urls_key_file_path_constants );
}
/**
* Get the constant used to define the private signing object prefix.
*
* @return string|false Constant name if defined, otherwise false
*/
public static function signed_urls_object_prefix_constant() {
return AS3CF_Utils::get_first_defined_constant( static::$signed_urls_object_prefix_constants );
}
/**
* Get a site specific file path example for a signed URL key file.
*
* @return string
*/
public static function signed_urls_key_file_path_placeholder() {
$filename = 'pk-12345678ABCDE.pem';
return dirname( ABSPATH ) . DIRECTORY_SEPARATOR . $filename;
}
/**
* Return a fully formed and potentially expiring signed URL for the given Item.
*
* @param Item $as3cf_item
* @param string $path The required bucket path, may differ from Item's path if image subsize etc.
* @param string $domain The domain to use for the URL if at all possible.
* @param string $scheme The scheme to be used if possible.
* @param array|null $headers Optional array of headers to be passed along to underlying requests.
*
* @return string
*/
public function get_url( Item $as3cf_item, $path, $domain, $scheme, $headers = array() ) {
$item_path = $this->as3cf->maybe_update_delivery_path( $path, $domain );
$item_path = AS3CF_Utils::encode_filename_in_path( $item_path );
return $scheme . '://' . $domain . '/' . $item_path;
}
/**
* Return a fully formed and expiring signed URL for the given Item.
*
* @param Item $as3cf_item
* @param string $path The required bucket path, may differ from Item's path if image subsize etc.
* @param string $domain The domain to use for the URL if at all possible.
* @param string $scheme The scheme to be used if possible.
* @param int $timestamp URL expires at the given time.
* @param array|null $headers Optional array of headers to be passed along to underlying requests.
*
* @return string
* @throws Exception
*/
public function get_signed_url( Item $as3cf_item, $path, $domain, $scheme, $timestamp, $headers = array() ) {
/**
* This default implementation defers to the storage provider's signed URLs.
* Therefore, we need to use a storage provider client instance for the item's region.
*/
if ( ! empty( $as3cf_item->region() ) && ( $this->as3cf->get_storage_provider()->region_required() || $this->as3cf->get_storage_provider()->get_default_region() !== $as3cf_item->region() ) ) {
$region = $this->as3cf->get_storage_provider()->sanitize_region( $as3cf_item->region() );
} else {
$region = '';
}
// Storage Provider may support signing custom domain, e.g. GCP.
if ( $this->as3cf->get_storage_provider()->get_domain() !== $domain ) {
$headers['BaseURL'] = $scheme . '://' . $domain;
}
return $this->as3cf->get_provider_client( $region )->get_object_url( $as3cf_item->bucket(), $path, $timestamp, $headers );
}
/**
* A short description of whether delivery is fast (distributed) or not.
*
* @return string
*/
public static function edge_server_support_desc() {
return __( 'Fast', 'amazon-s3-and-cloudfront' );
}
/**
* A short description of whether signed URLs for private media is supported or not.
*
* @return string
*/
public static function signed_urls_support_desc() {
return __( 'No Private Media', 'amazon-s3-and-cloudfront' );
}
/**
* Description for when Block All Public Access is enabled and Delivery Provider does not support it.
*
* @return string
*/
public static function get_block_public_access_enabled_unsupported_desc() {
return sprintf(
__( 'You need to disable Block All Public Access so that %1$s can access your bucket for delivery.', 'amazon-s3-and-cloudfront' ),
static::get_provider_name()
);
}
/**
* Prompt text to confirm that everything is in place to enable Block All Public Access without issues for Delivery Provider.
*
* @return string
*/
public static function get_block_public_access_confirm_setup_prompt() {
return '';
}
/**
* Description for when Object Ownership is enforced and Delivery Provider does not support it.
*
* @return string
*/
public static function get_object_ownership_enforced_unsupported_desc(): string {
global $as3cf;
$object_ownership_doc = $as3cf::dbrains_url(
'/wp-offload-media/doc/amazon-s3-bucket-object-ownership/',
array( 'utm_campaign' => 'support+docs', 'utm_content' => 'change+bucket+access' )
);
return sprintf(
__( 'You need to edit the bucket\'s Object Ownership setting and <a href="%1$s">enable ACLs</a> so that %2$s can access your bucket for delivery.', 'amazon-s3-and-cloudfront' ),
$object_ownership_doc,
static::get_provider_name()
);
}
/**
* Notice text for when a public file can't be accessed.
*
* @param string $error_message
*
* @return string
*/
public static function get_cannot_access_public_file_desc( string $error_message ): string {
return sprintf(
__(
'Offloaded media URLs may be broken. %1$s <a href="%2$s" target="_blank">Read more</a>',
'amazon-s3-and-cloudfront'
),
$error_message,
static::get_provider_service_quick_start_url()
);
}
/**
* Notice text for when a private file can't be accessed using a signed private URL.
*
* @param string $error_message
*
* @return string
*/
public static function get_cannot_access_private_file_desc( string $error_message ): string {
return sprintf(
__(
'Private offloaded media URLs may be broken. %1$s <a href="%2$s" target="_blank">Read more</a>',
'amazon-s3-and-cloudfront'
),
$error_message,
static::get_provider_service_quick_start_url()
);
}
/**
* Notice text for when a private file can be accessed using an unsigned URL.
*
* @return string
*/
public static function get_unsigned_url_can_access_private_file_desc(): string {
global $as3cf;
$storage_provider = $as3cf->get_storage_provider();
return apply_filters(
'as3cf_get_unsigned_url_can_access_private_file_desc_' . $storage_provider->get_provider_key_name(),
sprintf(
__(
'Delivery provider is connected, but private media is currently exposed through unsigned URLs. Restore privacy by verifying the configuration of private media settings. <a href="%1$s" target="_blank">Read more</a>',
'amazon-s3-and-cloudfront'
),
static::get_provider_service_quick_start_url()
)
);
}
/**
* Prompt text to confirm that everything is in place to enforce Object Ownership without issues for Delivery Provider.
*
* @return string
*/
public static function get_object_ownership_confirm_setup_prompt(): string {
// Using the same text as for the Block Public Access prompt.
return static::get_block_public_access_confirm_setup_prompt();
}
/**
* Get the link to the provider's console.
*
* @param string $bucket
* @param string $prefix
* @param string $region
*
* @return string
*
* NOTE: By default delivery providers append the suffix to the base console URL only.
* The usual bucket, prefix and region params are still processed and the suffix
* may end up being either a path, params or both that get deeper into the console.
*/
public function get_console_url( string $bucket = '', string $prefix = '', string $region = '' ): string {
if ( '' !== $prefix ) {
$prefix = $this->get_console_url_prefix_param() . apply_filters( 'as3cf_' . static::$provider_key_name . '_' . static::$service_key_name . '_console_url_prefix_value', $prefix );
}
$suffix = apply_filters( 'as3cf_' . static::$provider_key_name . '_' . static::$service_key_name . '_console_url_suffix_param', $this->get_console_url_suffix_param( $bucket, $prefix, $region ) );
return apply_filters( 'as3cf_' . static::$provider_key_name . '_' . static::$service_key_name . '_console_url', $this->console_url ) . $suffix;
}
/**
* 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 {
return '';
}
/**
* Validate delivery settings for the configured provider for the delivery status indicator.
*
* @param bool $force Force time resource consuming or state altering tests to run.
*
* @return AS3CF_Result
*/
public function validate_settings( bool $force = false ): AS3CF_Result {
$storage_provider = $this->as3cf->get_storage_provider();
$bucket = $this->as3cf->get_setting( 'bucket' );
$region = $this->as3cf->get_setting( 'region' );
$recommendations = array();
// Validate the delivery provider key.
$valid_delivery_provider_key = $this->validate_delivery_provider_key();
if ( Validator_Interface::AS3CF_STATUS_MESSAGE_SUCCESS !== $valid_delivery_provider_key->get_error_code() ) {
return $valid_delivery_provider_key;
}
// The storage provider has lower priority and runs before delivery, so we should always have a fresh result.
if ( $this->is_result_code_unknown_or_error( $this->as3cf->validation_manager->get_validation_status( 'storage' ) ) ) {
return new AS3CF_Result(
Validator_Interface::AS3CF_STATUS_MESSAGE_WARNING,
__( 'Delivery of offloaded media cannot be tested until the storage provider is successfully connected. See "Storage Settings" for more information.', 'amazon-s3-and-cloudfront' )
);
}
// Ensure the storage provider client is initiated before BAPA/OOE tests.
$storage_provider->get_client( array( 'region' => $region ) );
// If storage BAPA setting is enabled, validate that it's supported by delivery provider.
if ( ! static::block_public_access_supported() && $storage_provider->block_public_access_supported() && $storage_provider->public_access_blocked( $bucket ) ) {
return new AS3CF_Result(
Validator_Interface::AS3CF_STATUS_MESSAGE_ERROR,
sprintf(
_x(
'Offloaded media cannot be delivered because <strong>Block All Public Access</strong> is enabled. <a href="%1$s">Edit bucket security</a>',
'Delivery setting notice for issue with BAPA enabled on Storage Provider',
'amazon-s3-and-cloudfront'
),
'#/storage/security'
)
);
}
$delivery_domain_settings = $this->validate_delivery_domain();
if ( Validator_Interface::AS3CF_STATUS_MESSAGE_SUCCESS !== $delivery_domain_settings->get_error_code() ) {
return $delivery_domain_settings;
}
// Are settings for delivering signed URLs valid?
$signed_url_settings = $this->validate_signed_url_settings();
if ( Validator_Interface::AS3CF_STATUS_MESSAGE_SUCCESS !== $signed_url_settings->get_error_code() ) {
return $signed_url_settings;
}
// Test accessing files via provider.
$connection_test = $this->provider_connection_test();
if ( Validator_Interface::AS3CF_STATUS_MESSAGE_SUCCESS !== $connection_test->get_error_code() ) {
return $connection_test;
}
// Is Deliver Offloaded Media enabled?
$deliver_media = $this->validate_deliver_offloaded_media_enabled();
if ( Validator_Interface::AS3CF_STATUS_MESSAGE_SUCCESS !== $deliver_media->get_error_code() ) {
return $deliver_media;
}
// All good.
return new AS3CF_Result(
count( $recommendations ) === 0 ? Validator_Interface::AS3CF_STATUS_MESSAGE_SUCCESS : Validator_Interface::AS3CF_STATUS_MESSAGE_WARNING,
static::delivery_tests_pass_desc( $recommendations )
);
}
/**
* Validate that the delivery provider key provided in settings is valid for the storage provider. This should
* only happen if the user is using defines statements or has manually edited settings in the db.
*
* @return AS3CF_Result
*/
protected function validate_delivery_provider_key(): AS3CF_Result {
$storage_provider = $this->as3cf->get_storage_provider();
$storage_provider_key = $storage_provider->get_provider_key_name();
$delivery_provider_key = $this->as3cf->get_core_setting( 'delivery-provider' );
$valid_providers = array_keys( $this->as3cf->get_available_delivery_provider_details( $storage_provider_key ) );
if ( ! in_array( $delivery_provider_key, $valid_providers ) ) {
return new AS3CF_Result(
Validator_Interface::AS3CF_STATUS_MESSAGE_ERROR,
sprintf(
__( 'An invalid delivery provider has been defined for the active storage provider. Please use %1$s.', 'amazon-s3-and-cloudfront' ),
"<code>" . AS3CF_Utils::human_readable_join( "</code>, <code>", "</code> or <code>", $valid_providers ) . "</code>"
)
);
}
return new AS3CF_Result( Validator_Interface::AS3CF_STATUS_MESSAGE_SUCCESS );
}
/**
* Validate settings for serving signed URLs.
*
* @return AS3CF_Result
*/
protected function validate_signed_url_settings(): AS3CF_Result {
return new AS3CF_Result( Validator_Interface::AS3CF_STATUS_MESSAGE_SUCCESS );
}
/**
* Validate Deliver Offloaded Media is enabled.
*
* @return AS3CF_Result
*/
protected function validate_deliver_offloaded_media_enabled(): AS3CF_Result {
if ( ! $this->as3cf->get_setting( 'serve-from-s3' ) ) {
return new AS3CF_Result(
Validator_Interface::AS3CF_STATUS_MESSAGE_WARNING,
__(
'Delivery provider is successfully connected, but offloaded media will not be served until <strong>Deliver Offloaded Media</strong> is enabled. In the meantime, local media is being served if available.',
'amazon-s3-and-cloudfront'
)
);
}
return new AS3CF_Result( Validator_Interface::AS3CF_STATUS_MESSAGE_SUCCESS );
}
/**
* Test settings for custom delivery domain.
*
* @return AS3CF_Result
*/
protected function validate_delivery_domain(): AS3CF_Result {
$delivery_domain = $this->as3cf->get_setting( 'delivery-domain' );
$enable_delivery_domain = $this->as3cf->get_setting( 'enable-delivery-domain' );
if ( ! static::delivery_domain_allowed() ) {
return new AS3CF_Result( Validator_Interface::AS3CF_STATUS_MESSAGE_SUCCESS );
}
// Custom domain enabled?
if ( ! $enable_delivery_domain || empty( $delivery_domain ) ) {
return new AS3CF_Result(
Validator_Interface::AS3CF_STATUS_MESSAGE_WARNING,
sprintf(
__(
'Offloaded media cannot be delivered from the CDN until a delivery domain is set. <a href="%1$s" target="_blank">Read more</a>',
'amazon-s3-and-cloudfront'
),
static::get_provider_service_quick_start_url() . '#configure-plugin'
)
);
}
// Is the custom domain name valid?
$domain_check = new Domain_Check( $delivery_domain );
$domain_issue = $domain_check->get_validation_issue();
if ( ! empty( $domain_issue ) ) {
return new AS3CF_Result(
Validator_Interface::AS3CF_STATUS_MESSAGE_ERROR,
sprintf(
__(
'Offloaded media URLs may be broken due to an invalid delivery domain. %1$s <a href="%2$s">How to set a delivery domain</a>',
'amazon-s3-and-cloudfront'
),
$domain_issue,
static::get_provider_service_quick_start_url() . '#configure-plugin'
)
);
}
return new AS3CF_Result( Validator_Interface::AS3CF_STATUS_MESSAGE_SUCCESS );
}
/**
* Test public and private delivery from bucket using test files.
*
* @return AS3CF_Result
*/
protected function provider_connection_test(): AS3CF_Result {
$delivery_check = new Delivery_Check( $this->as3cf );
$setup_files = $delivery_check->setup_test_file( false );
if ( Validator_Interface::AS3CF_STATUS_MESSAGE_SUCCESS !== $setup_files->get_error_code() ) {
return new AS3CF_Result( $setup_files->get_error_code(), $setup_files->get_error_message() );
}
// Verify that the public file is accessible.
$delivery_issue = $delivery_check->test_public_file_access();
if ( Validator_Interface::AS3CF_STATUS_MESSAGE_SUCCESS !== $delivery_issue->get_error_code() ) {
$delivery_check->remove_test_files();
return new AS3CF_Result(
$delivery_issue->get_error_code(),
static::get_cannot_access_public_file_desc( $delivery_issue->get_error_message() )
);
}
$setup_files = $delivery_check->setup_test_file( true );
if ( Validator_Interface::AS3CF_STATUS_MESSAGE_SUCCESS !== $setup_files->get_error_code() ) {
return new AS3CF_Result( $setup_files->get_error_code(), $setup_files->get_error_message() );
}
// Verify that the private file is accessible.
$delivery_issue = $delivery_check->test_private_file_access();
if ( Validator_Interface::AS3CF_STATUS_MESSAGE_SUCCESS !== $delivery_issue->get_error_code() ) {
$delivery_check->remove_test_files();
return new AS3CF_Result(
$delivery_issue->get_error_code(),
static::get_cannot_access_private_file_desc( $delivery_issue->get_error_message() )
);
}
// Verify that the private file can't be accessed with unsigned URL.
$delivery_issue = $delivery_check->test_private_file_access_unsigned();
if ( Validator_Interface::AS3CF_STATUS_MESSAGE_SUCCESS !== $delivery_issue->get_error_code() ) {
$delivery_check->remove_test_files();
return new AS3CF_Result(
$delivery_issue->get_error_code(),
static::get_unsigned_url_can_access_private_file_desc()
);
}
// Ensure all test files are removed.
$delivery_check->remove_test_files();
return new AS3CF_Result( Validator_Interface::AS3CF_STATUS_MESSAGE_SUCCESS );
}
/**
* Get the name of the actions that are fired when the settings that the validator
* is responsible for are saved.
*
* @return array
*/
public function post_save_settings_actions(): array {
return array( 'as3cf_post_save_settings', 'as3cf_post_update_bucket' );
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Providers\Delivery;
class DigitalOcean_Spaces_CDN extends Delivery_Provider {
/**
* Which storage providers does the delivery provider support, empty means all.
*
* @var array
*/
protected static $supported_storage_providers = array(
'do',
);
/**
* @var string
*/
protected static $provider_name = 'DigitalOcean';
/**
* @var string
*/
protected static $provider_short_name = 'DigitalOcean';
/**
* Used in filters and settings.
*
* @var string
*/
protected static $provider_key_name = 'do';
/**
* @var string
*/
protected static $service_name = 'Spaces CDN';
/**
* @var string
*/
protected static $service_short_name = 'Spaces CDN';
/**
* Used in filters and settings.
*
* @var string
*/
protected static $service_key_name = 'spaces-cdn';
/**
* Optional override of "Provider Name" + "Service Name" for friendly name for service.
*
* @var string
*/
protected static $provider_service_name = 'DigitalOcean Spaces CDN';
/**
* The slug for the service's quick start guide doc.
*
* @var string
*/
protected static $provider_service_quick_start_slug = 'digitalocean-spaces-cdn-setup';
/**
* @var string
*/
protected $default_domain = 'cdn.digitaloceanspaces.com';
/**
* @var string
*/
protected $console_url = 'https://cloud.digitalocean.com/spaces/';
/**
* @var string
*/
protected $console_url_prefix_param = '?path=';
/**
* Get the link to the provider's console.
*
* @param string $bucket
* @param string $prefix
* @param string $region
*
* @return string
*
* NOTE: DigitalOcean Spaces CDN is tied to the Space and does not have a separate means of access.
*/
public function get_console_url( string $bucket = '', string $prefix = '', string $region = '' ): string {
return $this->as3cf->get_storage_provider()->get_console_url( $bucket, $prefix, $region );
}
/**
* Title to be shown for provider's console link.
*
* @return string
*/
public static function get_console_title(): string {
return _x( 'Control Panel', 'Provider console link text', 'amazon-s3-and-cloudfront' );
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Providers\Delivery;
class GCP_CDN extends Delivery_Provider {
/**
* Which storage providers does the delivery provider support, empty means all.
*
* @var array
*/
protected static $supported_storage_providers = array(
'gcp',
);
/**
* @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 = 'Cloud CDN';
/**
* @var string
*/
protected static $service_short_name = 'Cloud CDN';
/**
* Used in filters and settings.
*
* @var string
*/
protected static $service_key_name = 'cdn';
/**
* Optional override of "Provider Name" + "Service Name" for friendly name for service.
*
* @var string
*/
protected static $provider_service_name = 'GCP Cloud CDN';
/**
* The slug for the service's quick start guide doc.
*
* @var string
*/
protected static $provider_service_quick_start_slug = 'how-to-set-up-a-custom-domain-cdn-for-google-cloud-storage';
/**
* @var string
*/
protected $console_url = 'https://console.cloud.google.com/net-services/cdn/list';
/**
* @inheritDoc
*/
public static function signed_urls_support_desc() {
return __( 'Private Media Supported', 'amazon-s3-and-cloudfront' );
}
/**
* Title to be shown for provider's console link.
*
* @return string
*/
public static function get_console_title(): string {
return _x( 'Google Cloud Console', 'Provider console link text', 'amazon-s3-and-cloudfront' );
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Providers\Delivery;
class KeyCDN extends Delivery_Provider {
/**
* Which storage providers does the delivery provider support, empty means all.
*
* @var array
*/
protected static $supported_storage_providers = array(
// TODO: Add 'aws' after testing and documenting.
'do',
);
/**
* @var string
*/
protected static $provider_name = 'KeyCDN';
/**
* @var string
*/
protected static $provider_short_name = 'KeyCDN';
/**
* Used in filters and settings.
*
* @var string
*/
protected static $provider_key_name = 'keycdn';
/**
* @var string
*/
protected static $service_name = 'CDN';
/**
* @var string
*/
protected static $service_short_name = 'CDN';
/**
* Used in filters and settings.
*
* @var string
*/
protected static $service_key_name = 'cdn';
/**
* Optional override of "Provider Name" + "Service Name" for friendly name for service.
*
* @var string
*/
protected static $provider_service_name = 'KeyCDN';
/**
* @var string
*/
protected static $provider_service_quick_start_slug = 'how-to-set-up-a-custom-domain-for-digitalocean-spaces-with-keycdn';
/**
* @var string
*/
protected $console_url = 'https://app.keycdn.com/zones/index';
/**
* Title to be shown for provider's console link.
*
* @return string
*/
public static function get_console_title(): string {
return _x( 'KeyCDN Dashboard', 'Provider console link text', 'amazon-s3-and-cloudfront' );
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Providers\Delivery;
/**
* Class Other
*
* This is a fallback CDN where the user can add their own name.
*
* @package DeliciousBrains\WP_Offload_Media\Providers\Delivery
*/
class Other extends Delivery_Provider {
/**
* Can the displayed provider service name be overridden by the user?
*
* @var bool
*/
protected static $provider_service_name_override_allowed = true;
/**
* @var string
*/
protected static $provider_name = 'Other';
/**
* @var string
*/
protected static $provider_short_name = 'Other';
/**
* Used in filters and settings.
*
* @var string
*/
protected static $provider_key_name = 'other';
/**
* @var string
*/
protected static $service_name = 'Unknown';
/**
* @var string
*/
protected static $service_short_name = 'Unknown';
/**
* Used in filters and settings.
*
* @var string
*/
protected static $service_key_name = 'unknown';
/**
* Optional override of "Provider Name" + "Service Name" for friendly name for service.
*
* @var string
*/
protected static $provider_service_name = 'Other';
}

View File

@@ -0,0 +1,76 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Providers\Delivery;
class StackPath extends Delivery_Provider {
/**
* Which storage providers does the delivery provider support, empty means all.
*
* @var array
*/
protected static $supported_storage_providers = array(
// TODO: Add 'do' after testing and documenting.
'aws',
);
/**
* @var string
*/
protected static $provider_name = 'StackPath';
/**
* @var string
*/
protected static $provider_short_name = 'StackPath';
/**
* Used in filters and settings.
*
* @var string
*/
protected static $provider_key_name = 'stackpath';
/**
* @var string
*/
protected static $service_name = 'CDN';
/**
* @var string
*/
protected static $service_short_name = 'CDN';
/**
* Used in filters and settings.
*
* @var string
*/
protected static $service_key_name = 'cdn';
/**
* Optional override of "Provider Name" + "Service Name" for friendly name for service.
*
* @var string
*/
protected static $provider_service_name = 'StackPath';
/**
* @var string
*/
protected static $provider_service_quick_start_slug = 'stackpath-setup';
/**
* @var string
*/
protected $console_url = 'https://control.stackpath.com/stacks';
/**
* Title to be shown for provider's console link.
*
* @return string
*/
public static function get_console_title(): string {
return _x( 'Control Portal', 'Provider console link text', 'amazon-s3-and-cloudfront' );
}
}

View File

@@ -0,0 +1,161 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Providers\Delivery;
use DeliciousBrains\WP_Offload_Media\Items\Item;
class Storage extends Delivery_Provider {
/**
* @var bool
*/
protected static $delivery_domain_allowed = false;
/**
* @var string
*/
protected static $provider_name = 'Storage Provider';
/**
* @var string
*/
protected static $provider_short_name = 'Storage';
/**
* Used in filters and settings.
*
* @var string
*/
protected static $provider_key_name = 'storage';
/**
* @var string
*/
protected static $service_name = 'Bucket';
/**
* @var string
*/
protected static $service_short_name = 'Bucket';
/**
* Used in filters and settings.
*
* @var string
*/
protected static $service_key_name = 'bucket';
/**
* Returns the full name for the provider.
*
* @return string
*/
public static function get_provider_name() {
/** @var \Amazon_S3_And_CloudFront $as3cf */
global $as3cf;
return $as3cf->get_storage_provider()->get_provider_name();
}
/**
* Returns the full name for the service.
*
* @return string
*/
public static function get_service_name() {
/** @var \Amazon_S3_And_CloudFront $as3cf */
global $as3cf;
return $as3cf->get_storage_provider()->get_service_name();
}
/**
* Returns the full name for the provider and service for display.
*
* @param bool $override_allowed Not used.
*
* @return string
*/
public static function get_provider_service_name( $override_allowed = true ) {
/** @var \Amazon_S3_And_CloudFront $as3cf */
global $as3cf;
return $as3cf->get_storage_provider()->get_provider_service_name();
}
/**
* {@inheritDoc}
*/
public static function edge_server_support_desc() {
return __( 'Slow', 'amazon-s3-and-cloudfront' );
}
/**
* {@inheritDoc}
*/
public static function signed_urls_support_desc() {
return __( 'Private Media Supported', 'amazon-s3-and-cloudfront' );
}
/**
* {@inheritDoc}
*/
public function get_url( Item $as3cf_item, $path, $domain, $scheme, $headers = array() ) {
$lookup_domain = $this->as3cf->get_storage_provider_instance( $as3cf_item->provider() )->get_url_domain( $as3cf_item->bucket(), $as3cf_item->region() );
return parent::get_url( $as3cf_item, $path, $lookup_domain, $scheme, $headers );
}
/**
* Description for when Block All Public Access is enabled and Delivery Provider does not support it.
*
* @return string
*/
public static function get_block_public_access_enabled_unsupported_desc() {
return __( 'You need to disable Block All Public Access so that your bucket is accessible for delivery.', 'amazon-s3-and-cloudfront' );
}
/**
* Description for when Object Ownership is enforced and Delivery Provider does not support it.
*
* @return string
*/
public static function get_object_ownership_enforced_unsupported_desc(): string {
global $as3cf;
$object_ownership_doc = $as3cf::dbrains_url(
'/wp-offload-media/doc/amazon-s3-bucket-object-ownership/',
array( 'utm_campaign' => 'support+docs', 'utm_content' => 'change+bucket+access' )
);
return sprintf(
__( 'You need to edit the bucket\'s Object Ownership setting and <a href="%1$s">enable ACLs</a> or add a <a href="%2$s">Bucket Policy</a> so that objects can be made available for delivery.', 'amazon-s3-and-cloudfront' ),
$object_ownership_doc . '#acls',
$object_ownership_doc . '#bucket-policy'
);
}
/**
* Get the link to the provider's console.
*
* @param string $bucket
* @param string $prefix
* @param string $region
*
* @return string
*/
public function get_console_url( string $bucket = '', string $prefix = '', string $region = '' ): string {
return $this->as3cf->get_storage_provider()->get_console_url( $bucket, $prefix, $region );
}
/**
* Title to be shown for provider's console link.
*
* @return string
*/
public static function get_console_title(): string {
global $as3cf;
return $as3cf->get_storage_provider()->get_console_title();
}
}

View File

@@ -0,0 +1,396 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Providers;
use Amazon_S3_And_CloudFront;
use AS3CF_Plugin_Base;
abstract class Provider {
/**
* @var Amazon_S3_And_CloudFront
*/
protected $as3cf;
/**
* Can the displayed provider service name be overridden by the user?
*
* @var bool
*/
protected static $provider_service_name_override_allowed = false;
/**
* @var string
*/
protected static $provider_service_name_setting_name = 'provider-service-name';
/**
* @var string
*/
protected static $provider_name = '';
/**
* @var string
*/
protected static $provider_short_name = '';
/**
* Used in filters and settings.
*
* @var string
*/
protected static $provider_key_name = '';
/**
* @var string
*/
protected static $service_name = '';
/**
* @var string
*/
protected static $service_short_name = '';
/**
* Used in filters and settings.
*
* @var string
*/
protected static $service_key_name = '';
/**
* Optional override of "Provider Name" + "Service Name" for friendly name for service.
*
* @var string
*/
protected static $provider_service_name = '';
/**
* The slug for the service's quick start guide doc.
*
* @var string
*/
protected static $provider_service_quick_start_slug = 'quick-start-guide';
/**
* @var string
*/
protected $default_domain = '';
/**
* @var string
*/
protected $console_url = '';
/**
* @var string
*/
protected $console_url_prefix_param = '';
/**
* @var bool
*/
protected static $block_public_access_supported = false;
/**
* @var bool
*/
protected static $object_ownership_supported = false;
/**
* Provider constructor.
*
* @param AS3CF_Plugin_Base $as3cf
*/
public function __construct( AS3CF_Plugin_Base $as3cf ) {
$this->as3cf = $as3cf;
}
/**
* Returns the full name for the provider.
*
* @return string
*/
public static function get_provider_name() {
return static::$provider_name;
}
/**
* Returns the key friendly name for the provider.
*
* @return string
*/
public static function get_provider_key_name() {
return static::$provider_key_name;
}
/**
* Returns the full name for the service.
*
* @return string
*/
public static function get_service_name() {
return static::$service_name;
}
/**
* Returns the key friendly name for the service.
*
* @return string
*/
public static function get_service_key_name() {
return static::$service_key_name;
}
/**
* Returns the full name for the provider and service for display.
*
* @param bool $override_allowed Use override if available? Defaults to true.
*
* @return string
*/
public static function get_provider_service_name( $override_allowed = true ) {
if ( ! empty( static::$provider_service_name ) ) {
$result = static::$provider_service_name;
} else {
$result = static::$provider_name . ' ' . static::$service_name;
}
if ( false === $override_allowed || false === static::provider_service_name_override_allowed() ) {
return $result;
} else {
/** @var Amazon_S3_And_CloudFront $as3cf */
global $as3cf;
$override = stripslashes( $as3cf->get_setting( static::$provider_service_name_setting_name ) );
if ( empty( $override ) ) {
return $result;
} else {
return $override;
}
}
}
/**
* Returns the full name for the provider and service for use as logo's alt text.
*
* @param bool $override_allowed Use override if available? Defaults to true.
*
* @return string
*
* Note: For accessibility reasons, returned string should differ from `get_provider_service_name`.
*/
public static function get_icon_desc( $override_allowed = true ) {
return sprintf( _x( '%s logo', 'Provider icon\'s alt text', 'amazon-s3-and-cloudfront' ), static::get_provider_service_name( $override_allowed ) );
}
/**
* Can the displayed provider service name be overridden by the user?
*
* @return bool
*/
public static function provider_service_name_override_allowed() {
return static::$provider_service_name_override_allowed;
}
/**
* Returns the slug for the service's quick start guide doc.
*
* @return string
*/
public static function get_provider_service_quick_start_slug() {
return static::$provider_service_quick_start_slug;
}
/**
* Returns URL for quick start guide.
*
* @return string
*/
public static function get_provider_service_quick_start_url() {
global $as3cf;
return $as3cf::dbrains_url( '/wp-offload-media/doc/' . static::get_provider_service_quick_start_slug() );
}
/**
* Returns the Provider's base domain.
*
* Does not include region prefix or bucket path etc.
*
* @return string
*/
public function get_domain() {
return apply_filters( 'as3cf_' . static::$provider_key_name . '_' . static::$service_key_name . '_domain', $this->default_domain );
}
/**
* Get the link to the provider's console.
*
* @param string $bucket
* @param string $prefix
* @param string $region
*
* @return string
*/
public function get_console_url( string $bucket = '', string $prefix = '', string $region = '' ): string {
if ( '' !== $prefix ) {
$prefix = $this->get_console_url_prefix_param() . apply_filters( 'as3cf_' . static::$provider_key_name . '_' . static::$service_key_name . '_console_url_prefix_value', $prefix );
}
$suffix = apply_filters( 'as3cf_' . static::$provider_key_name . '_' . static::$service_key_name . '_console_url_suffix_param', $this->get_console_url_suffix_param( $bucket, $prefix, $region ) );
return apply_filters( 'as3cf_' . static::$provider_key_name . '_' . static::$service_key_name . '_console_url', $this->console_url ) . $bucket . $prefix . $suffix;
}
/**
* Get the prefix param to append to the link to the provider's console.
*
* @return string
*/
public function get_console_url_prefix_param(): string {
return apply_filters( 'as3cf_' . static::$provider_key_name . '_' . static::$service_key_name . '_console_url_prefix_param', $this->console_url_prefix_param );
}
/**
* Title to be shown for provider's console link.
*
* @return string
*/
public static function get_console_title(): string {
return _x( 'Provider Console', 'Default provider console link text', 'amazon-s3-and-cloudfront' );
}
/**
* Does provider support blocking direct public access to bucket?
*
* @return bool
*/
public static function block_public_access_supported() {
return static::$block_public_access_supported;
}
/**
* Description for when Block All Public Access is enabled and Delivery Provider supports it.
*
* @return string
*/
public static function get_block_public_access_enabled_supported_desc() {
return '';
}
/**
* Description for when Block All Public Access is enabled and Delivery Provider does not support it.
*
* @return string
*/
public static function get_block_public_access_enabled_unsupported_desc() {
return '';
}
/**
* Description for when Block All Public Access is enabled during initial setup.
*
* @return string
*/
public static function get_block_public_access_enabled_unsupported_setup_desc() {
return '';
}
/**
* Description for when Block All Public Access is disabled and Delivery Provider supports it.
*
* @return string
*/
public static function get_block_public_access_disabled_supported_desc() {
return '';
}
/**
* Description for when Block All Public Access is disabled and Delivery Provider does not support it.
*
* @return string
*/
public static function get_block_public_access_disabled_unsupported_desc() {
return '';
}
/**
* Does provider support object ownership controls?
*
* @return bool
*/
public static function object_ownership_supported(): bool {
return static::$object_ownership_supported;
}
/**
* Description for when Object Ownership is enforced and Delivery Provider supports it.
*
* @return string
*/
public static function get_object_ownership_enforced_supported_desc(): string {
return '';
}
/**
* Description for when Object Ownership is enforced and Delivery Provider does not support it.
*
* @return string
*/
public static function get_object_ownership_enforced_unsupported_desc(): string {
return '';
}
/**
* Description for when Object Ownership is enforced during initial setup.
*
* @return string
*/
public static function get_object_ownership_enforced_unsupported_setup_desc(): string {
return '';
}
/**
* Description for when Object Ownership is not enforced and Delivery Provider supports it.
*
* @return string
*/
public static function get_object_ownership_not_enforced_supported_desc(): string {
return '';
}
/**
* Description for when Object Ownership is not enforced and Delivery Provider does not support it.
*
* @return string
*/
public static function get_object_ownership_not_enforced_unsupported_desc(): string {
return '';
}
/**
* Does the provider require ACLs to be used?
*
* @return bool
*/
public static function requires_acls(): bool {
if ( static::block_public_access_supported() || static::object_ownership_supported() ) {
return false;
}
return true;
}
/**
* 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
*/
abstract protected function get_console_url_suffix_param( string $bucket = '', string $prefix = '', string $region = '' ): string;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,249 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Providers\Storage;
use Exception;
class DigitalOcean_Provider extends AWS_Provider {
/**
* @var string
*/
protected static $provider_name = 'DigitalOcean';
/**
* @var string
*/
protected static $provider_short_name = 'DigitalOcean';
/**
* Used in filters and settings.
*
* @var string
*/
protected static $provider_key_name = 'do';
/**
* @var string
*/
protected static $service_name = 'Spaces';
/**
* @var string
*/
protected static $service_short_name = 'Spaces';
/**
* Used in filters and settings.
*
* @var string
*/
protected static $service_key_name = 'spaces';
/**
* Optional override of "Provider Name" + "Service Name" for friendly name for service.
*
* @var string
*/
protected static $provider_service_name = '';
/**
* The slug for the service's quick start guide doc.
*
* @var string
*/
protected static $provider_service_quick_start_slug = 'digitalocean-spaces-quick-start-guide';
/**
* @var array
*/
protected static $access_key_id_constants = array(
'AS3CF_DO_ACCESS_KEY_ID',
);
/**
* @var array
*/
protected static $secret_access_key_constants = array(
'AS3CF_DO_SECRET_ACCESS_KEY',
);
/**
* @var array
*/
protected static $use_server_roles_constants = array();
/**
* @var bool
*/
protected static $block_public_access_supported = false;
/**
* @var bool
*/
protected static $object_ownership_supported = false;
/**
* @var array
*/
protected static $regions = array(
'nyc3' => 'New York',
'ams3' => 'Amsterdam',
'sgp1' => 'Singapore',
'sfo2' => 'San Francisco 2',
'sfo3' => 'San Francisco 3',
'fra1' => 'Frankfurt',
'blr1' => 'Bangalore',
'syd1' => 'Sydney',
'lon1' => 'London',
'tor1' => 'Toronto',
);
/**
* @var bool
*/
protected static $region_required = true;
/**
* @var string
*/
protected static $default_region = 'nyc3';
/**
* @var string
*/
protected $default_domain = 'digitaloceanspaces.com';
/**
* @var string
*/
protected $console_url = 'https://cloud.digitalocean.com/spaces/';
/**
* @var string
*/
protected $console_url_prefix_param = '?path=';
/**
* @var array
*/
private $client_args = 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 ) {
if ( empty( $args['endpoint'] ) ) {
// DigitalOcean endpoints always require a region.
$args['region'] = empty( $args['region'] ) ? static::get_default_region() : $args['region'];
$args['endpoint'] = 'https://' . $args['region'] . '.' . $this->get_domain();
}
$this->client_args = $args;
return $this->client_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;
}
/**
* Update the block public access setting for the given bucket.
*
* @param string $bucket
* @param bool $block
*/
public function block_public_access( string $bucket, bool $block ) {
// DigitalOcean doesn't support this, so do nothing.
}
/**
* Update the object ownership enforced setting for the given bucket.
*
* @param string $bucket
* @param bool $enforce
*/
public function enforce_object_ownership( string $bucket, bool $enforce ) {
// DigitalOcean doesn't support this, so do nothing.
}
/**
* Create bucket.
*
* @param array $args
*
* @throws Exception
*/
public function create_bucket( array $args ) {
// DigitalOcean requests always require a region, and it must be an AWS S3 one.
// The region in the endpoint is all that matters to DigitalOcean Spaces.
// @see https://docs.digitalocean.com/products/spaces/reference/s3-sdk-examples/#configure-a-client
if ( ! empty( $this->client_args['region'] ) && 'us-east-1' === $this->client_args['region'] ) {
parent::create_bucket( $args );
} else {
$client_args = $this->client_args;
$client_args['region'] = 'us-east-1';
unset( $args['LocationConstraint'] ); // Not needed and breaks signature.
$this->get_client( $client_args, true )->create_bucket( $args );
}
}
/**
* Returns region for bucket.
*
* @param array $args
*
* @return string
*/
public function get_bucket_location( array $args ) {
// For some reason DigitalOcean Spaces returns an XML LocationConstraint segment prepended to the region key.
return strip_tags( parent::get_bucket_location( $args ) );
}
/**
* 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 $region;
}
/**
* 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 {
return '';
}
/**
* Title to be shown for provider's console link.
*
* @return string
*/
public static function get_console_title(): string {
return _x( 'Control Panel', 'Provider console link text', 'amazon-s3-and-cloudfront' );
}
}

View File

@@ -0,0 +1,911 @@
<?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 );
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Providers\Storage;
use AS3CF_Error;
use Exception;
class Null_Provider {
/**
* Log and fail calls to instance methods.
*
* @param string $name
* @param array $arguments
*
* @throws Exception
*/
public function __call( $name, $arguments ) {
AS3CF_Error::log( $arguments, __CLASS__ . "->$name()" );
throw new Exception( 'Failed to instantiate the provider client. Check your error log. Function called:- ' . __CLASS__ . "->$name()" );
}
/**
* Log and fail calls to static methods.
*
* @param string $name
* @param array $arguments
*
* @throws Exception
*/
public static function __callStatic( $name, $arguments ) {
AS3CF_Error::log( $arguments, __CLASS__ . "::$name()" );
throw new Exception( 'Failed to instantiate the provider client. Check your error log. Function called:- ' . __CLASS__ . "->$name()" );
}
}

View File

@@ -0,0 +1,237 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Providers\Storage;
use Exception;
class S3_Compatible_Provider extends AWS_Provider {
/**
* @var string
*/
protected static $provider_name = 'S3-Compatible';
/**
* @var string
*/
protected static $provider_short_name = 'S3 Compatible';
/**
* Used in filters and settings.
*
* @var string
*/
protected static $provider_key_name = 's3compatible';
/**
* @var string
*/
protected static $service_name = 'S3-Compatible Storage';
/**
* @var string
*/
protected static $service_short_name = 'S3';
/**
* Used in filters and settings.
*
* @var string
*/
protected static $service_key_name = 's3compat';
/**
* Optional override of "Provider Name" + "Service Name" for friendly name for service.
*
* @var string
*/
protected static $provider_service_name = 'S3-Compatible Storage';
/**
* The slug for the service's quick start guide doc.
*
* @var string
*/
protected static $provider_service_quick_start_slug = '';
/**
* @var array
*/
protected static $access_key_id_constants = array(
'AS3CF_S3COMPAT_ACCESS_KEY_ID',
);
/**
* @var array
*/
protected static $secret_access_key_constants = array(
'AS3CF_S3COMPAT_SECRET_ACCESS_KEY',
);
/**
* Server roles not supported for generic S3-compatible services.
*
* @var array
*/
protected static $use_server_roles_constants = array();
/**
* Block Public Access is not universally supported by S3-compatible services.
*
* @var bool
*/
protected static $block_public_access_supported = false;
/**
* Object Ownership is not universally supported by S3-compatible services.
*
* @var bool
*/
protected static $object_ownership_supported = false;
/**
* S3-compatible services use free-form region strings; no fixed list.
*
* @var array
*/
protected static $regions = array();
/**
* @var bool
*/
protected static $region_required = false;
/**
* Default region required by the AWS SDK, even for S3-compatible services.
*
* @var string
*/
protected static $default_region = 'us-east-1';
/**
* No fixed default domain; derived from the configured endpoint.
*
* @var string
*/
protected $default_domain = '';
/**
* @var string
*/
protected $console_url = '';
/**
* @var string
*/
protected $console_url_prefix_param = '/';
/**
* Process the args before instantiating a new client for the provider's SDK.
* Injects the custom endpoint and enables path-style access.
*
* @param array $args
*
* @return array
*/
protected function init_client_args( array $args ) {
$endpoint = $this->as3cf->get_setting( 'endpoint' );
if ( ! empty( $endpoint ) ) {
$args['endpoint'] = rtrim( $endpoint, '/' );
$args['use_path_style_endpoint'] = true;
}
return $args;
}
/**
* Returns the host (and port if non-standard) extracted from the configured endpoint URL.
*
* @return string
*/
public function get_domain() {
$endpoint = $this->as3cf->get_setting( 'endpoint' );
if ( empty( $endpoint ) ) {
return '';
}
$parsed = parse_url( $endpoint );
$host = isset( $parsed['host'] ) ? $parsed['host'] : '';
if ( ! empty( $parsed['port'] ) ) {
$host .= ':' . $parsed['port'];
}
return $host;
}
/**
* No region-based prefix is needed for S3-compatible services.
*
* @param string $region
* @param null|int $expires
*
* @return string
*/
protected function url_prefix( $region = '', $expires = null ) {
return '';
}
/**
* Always use path-style URLs: endpoint-host/bucket instead of bucket.endpoint-host.
*
* @param string $domain
* @param string $bucket
* @param string $region
* @param int $expires
* @param array $args
*
* @return string
*/
protected function url_domain( $domain, $bucket, $region = '', $expires = null, $args = array() ) {
return $domain . '/' . $bucket;
}
/**
* Create bucket without a LocationConstraint, which many S3-compatible
* services do not support.
*
* @param array $args
*/
public function create_bucket( array $args ) {
// Remove AWS-specific location constraint; S3-compatible services
// typically don't support it and use the endpoint region instead.
unset( $args['LocationConstraint'] );
parent::create_bucket( $args );
}
/**
* Block Public Access is not supported by most S3-compatible services.
*
* @param string $bucket
* @param bool $block
*/
public function block_public_access( string $bucket, bool $block ) {
// Not supported — do nothing.
}
/**
* Object Ownership enforcement is not supported by most S3-compatible services.
*
* @param string $bucket
* @param bool $enforce
*/
public function enforce_object_ownership( string $bucket, bool $enforce ) {
// Not supported — do nothing.
}
/**
* Returns the console title (not applicable for generic S3-compatible services).
*
* @return string
*/
public static function get_console_title(): string {
return _x( 'Console', 'Provider console link text', 'amazon-s3-and-cloudfront' );
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,116 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Providers\Storage\Streams;
use DeliciousBrains\WP_Offload_Media\Aws3\Aws\CacheInterface;
use DeliciousBrains\WP_Offload_Media\Aws3\Aws\S3\S3ClientInterface;
use DeliciousBrains\WP_Offload_Media\Aws3\Aws\S3\StreamWrapper;
class AWS_S3_Stream_Wrapper extends StreamWrapper {
public static $wrapper;
/**
* Register the 's3://' stream wrapper
*
* @param S3ClientInterface $client Client to use with the stream wrapper
* @param string $protocol Protocol to register as.
* @param CacheInterface $cache Default cache for the protocol.
* @param bool $v2_existence Whether or not to use V2 bucket and object existence methods
*/
public static function register( S3ClientInterface $client, $protocol = 's3', CacheInterface $cache = null, $v2_existence = false ) {
// Keep a shadow copy of the protocol for use with context options.
static::$wrapper = $protocol;
parent::register( $client, $protocol, $cache, $v2_existence );
}
/**
* Overrides so we don't check for stat on directories
*
* @param string $path
* @param int $flags
*
* @return array
*/
public function url_stat( $path, $flags ) {
$extension = pathinfo( $path, PATHINFO_EXTENSION );
// If the path is a directory then return it as always existing.
if ( ! $extension ) {
return array(
0 => 0,
'dev' => 0,
1 => 0,
'ino' => 0,
2 => 16895,
'mode' => 16895,
3 => 0,
'nlink' => 0,
4 => 0,
'uid' => 0,
5 => 0,
'gid' => 0,
6 => -1,
'rdev' => -1,
7 => 0,
'size' => 0,
8 => 0,
'atime' => 0,
9 => 0,
'mtime' => 0,
10 => 0,
'ctime' => 0,
11 => -1,
'blksize' => -1,
12 => -1,
'blocks' => -1,
);
}
return parent::url_stat( $path, $flags );
}
/**
* Override the S3 Put Object arguments
*
* @return bool
*/
public function stream_flush() {
/** @var \Amazon_S3_And_CloudFront|\Amazon_S3_And_CloudFront_Pro $as3cf */
global $as3cf;
if ( $as3cf->get_setting( 'use-bucket-acls' ) ) {
$context = stream_context_get_default();
if ( null !== $this->context ) {
$context = $this->context;
}
$options = stream_context_get_options( $context );
// Set the ACL, usually defaults to public.
$provider = $as3cf->get_storage_provider();
$options[ static::$wrapper ]['ACL'] = $provider->get_default_acl();
$options = apply_filters( 'wpos3_stream_flush_params', $options ); // Backwards compatibility
$options = apply_filters( 'as3cf_stream_flush_params', $options );
stream_context_set_option( $context, $options );
}
return parent::stream_flush();
}
/**
* Dummy function to stop PHP from throwing a wobbly.
*
* @param string $path
* @param int $option
* @param mixed $value
*
* @return bool
*/
public function stream_metadata( $path, $option, $value ) {
return true;
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Providers\Storage\Streams;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Storage\StorageClient;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Storage\StreamWrapper;
class GCP_GCS_Stream_Wrapper extends StreamWrapper {
public static $wrapper;
/**
* Register the 'gs://' stream wrapper
*
* @param StorageClient $client Client to use with the stream wrapper
* @param string $protocol Protocol to register as.
*/
public static function register( StorageClient $client, $protocol = 'gs' ) {
// Keep a shadow copy of the protocol for use with context options.
static::$wrapper = $protocol;
parent::register( $client, $protocol );
}
/**
* Overrides so we don't check for stat on directories
*
* @param string $path
* @param int $flags
*
* @return array
*/
public function url_stat( $path, $flags ) {
$extension = pathinfo( $path, PATHINFO_EXTENSION );
// If the path is a directory then return it as always existing.
if ( ! $extension ) {
return array(
0 => 0,
'dev' => 0,
1 => 0,
'ino' => 0,
2 => 16895,
'mode' => 16895,
3 => 0,
'nlink' => 0,
4 => 0,
'uid' => 0,
5 => 0,
'gid' => 0,
6 => -1,
'rdev' => -1,
7 => 0,
'size' => 0,
8 => 0,
'atime' => 0,
9 => 0,
'mtime' => 0,
10 => 0,
'ctime' => 0,
11 => -1,
'blksize' => -1,
12 => -1,
'blocks' => -1,
);
}
return parent::url_stat( $path, $flags );
}
}