How to set a custom domain name', '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 .= '

'; $message .= join( '

', $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 enable ACLs 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 Read more', '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 Read more', '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. Read more', '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 Block All Public Access is enabled. Edit bucket security', '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' ), "" . AS3CF_Utils::human_readable_join( ", ", " or ", $valid_providers ) . "" ) ); } 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 Deliver Offloaded Media 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. Read more', '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 How to set a delivery domain', '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' ); } }