From 4e27924a9050bc9492e4b62046bba0d875e3259b Mon Sep 17 00:00:00 2001 From: "Ian M. Jones" Date: Wed, 9 Mar 2022 14:19:18 +0000 Subject: [PATCH] Deploying version 2.6.0 --- README.md | 22 +- classes/amazon-s3-and-cloudfront.php | 2267 +++-------------- classes/as3cf-filter.php | 251 +- classes/as3cf-plugin-compatibility.php | 268 +- classes/as3cf-utils.php | 158 +- classes/filters/as3cf-local-to-s3.php | 184 +- classes/filters/as3cf-s3-to-local.php | 126 +- classes/integrations/core.php | 43 + classes/integrations/integration-manager.php | 81 + classes/integrations/integration.php | 37 + classes/integrations/media-library.php | 1551 +++++++++++ classes/items/download-handler.php | 169 ++ classes/items/item-handler.php | 247 ++ classes/items/item.php | 1054 +++++++- classes/items/manifest.php | 10 + classes/items/media-library-item.php | 659 +++-- classes/items/remove-local-handler.php | 209 ++ classes/items/remove-provider-handler.php | 141 + classes/items/upload-handler.php | 449 ++++ .../providers/storage/storage-provider.php | 7 +- classes/upgrades/upgrade-edd-replace-urls.php | 9 +- classes/upgrades/upgrade-file-sizes.php | 23 +- classes/upgrades/upgrade-filter-post.php | 33 +- classes/upgrades/upgrade-item-extra-data.php | 152 ++ classes/upgrades/upgrade-items-table.php | 19 +- classes/upgrades/upgrade-meta-wp-error.php | 14 +- classes/upgrades/upgrade-region-meta.php | 16 +- classes/upgrades/upgrade-tools-errors.php | 105 + classes/upgrades/upgrade.php | 19 +- include/functions.php | 16 +- languages/amazon-s3-and-cloudfront-en.pot | 403 +-- readme.txt | 22 +- view/attachment-metabox.php | 39 +- wordpress-s3.php | 6 +- 34 files changed, 5791 insertions(+), 3018 deletions(-) create mode 100644 classes/integrations/core.php create mode 100644 classes/integrations/integration-manager.php create mode 100644 classes/integrations/integration.php create mode 100644 classes/integrations/media-library.php create mode 100644 classes/items/download-handler.php create mode 100644 classes/items/item-handler.php create mode 100644 classes/items/manifest.php create mode 100644 classes/items/remove-local-handler.php create mode 100644 classes/items/remove-provider-handler.php create mode 100644 classes/items/upload-handler.php create mode 100644 classes/upgrades/upgrade-item-extra-data.php create mode 100644 classes/upgrades/upgrade-tools-errors.php diff --git a/README.md b/README.md index 52acbdf2..5b67ae4a 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ **Contributors:** bradt, deliciousbrains, ianmjones **Tags:** uploads, amazon, s3, amazon s3, digitalocean, digitalocean spaces, google cloud storage, gcs, mirror, admin, media, cdn, cloudfront **Requires at least:** 4.9 -**Tested up to:** 5.8 -**Requires PHP:** 5.5 -**Stable tag:** 2.5.5 +**Tested up to:** 5.9 +**Requires PHP:** 5.6 +**Stable tag:** 2.6.0 **License:** GPLv3 Copies files to Amazon S3, DigitalOcean Spaces or Google Cloud Storage as they are uploaded to the Media Library. Optionally configure Amazon CloudFront or another CDN for faster delivery. @@ -13,7 +13,7 @@ Copies files to Amazon S3, DigitalOcean Spaces or Google Cloud Storage as they a FORMERLY WP OFFLOAD S3 LITE -https://www.youtube.com/watch?v=_PVybEGaRXc +https://www.youtube.com/watch?v=I-wTMXMeFu4 This plugin automatically copies images, videos, documents, and any other media added through WordPress' media uploader to [Amazon S3](http://aws.amazon.com/s3/), [DigitalOcean Spaces](https://www.digitalocean.com/products/spaces/) or [Google Cloud Storage](https://cloud.google.com/storage/). It then automatically replaces the URL to each media file with their respective Amazon S3, DigitalOcean Spaces or Google Cloud Storage URL or, if you have configured [Amazon CloudFront](http://aws.amazon.com/cloudfront/) or another CDN with or without a custom domain, that URL instead. Image thumbnails are also copied to the bucket and delivered through the correct remote URL. @@ -75,6 +75,10 @@ If you upgrade to the pro version of [WP Offload Media](https://deliciousbrains. ## Upgrade Notice ## +### 2.6 ### +This is a major upgrade that updates the format of information stored about offloaded Media Library items. Once upgraded you will not be able to downgrade without restoring data from a backup. +This version requires PHP 5.6+ + ### 2.3 ### This is a major upgrade that switches to using a custom table for storing data about offloaded Media Library items. Once upgraded you will not be able to downgrade without restoring data from a backup. @@ -89,6 +93,16 @@ This version requires PHP 5.3.3+ and the Amazon Web Services plugin ## Changelog ## +### WP Offload Media Lite 2.6 - 2022-03-09 ### +* [Release Summary Blog Post](https://deliciousbrains.com/wp-offload-media-2-6-released/?utm_campaign=changelogs&utm_source=wordpress.org&utm_medium=free%2Bplugin%2Blisting) +* New: WP Offload Media is now compatible with WordPress 5.9 and Full Site Editing +* Improvement: Offloaded thumbnail sizes are now tracked for better handling of changes to registered sizes +* Improvement: Offloads and other storage provider actions are faster +* Bug fix: URL rewriting now works in the Full Site Editor +* Bug fix: Offloaded images are now shown when re-editing a Block Template or Template Part +* Bug fix: URL rewriting now works for Widgets migrated to a Widget Sidebar Block +* Bug fix: Objects are no longer left in the bucket when deleting a Media Library item with many changes to its thumbnail sizes + ### WP Offload Media Lite 2.5.5 - 2021-07-19 ### * Bug fix: Signed GCS URLs broken when updating a post * Bug fix: Incorrect mime type set on scaled image's bucket object when thumbnail format differs from original file's format diff --git a/classes/amazon-s3-and-cloudfront.php b/classes/amazon-s3-and-cloudfront.php index 434423d8..efb8a882 100644 --- a/classes/amazon-s3-and-cloudfront.php +++ b/classes/amazon-s3-and-cloudfront.php @@ -1,7 +1,15 @@ 'DeliciousBrains\WP_Offload_Media\Items\Media_Library_Item', + ); + + /** + * @var Integration_Manager + */ + protected $integration_manager; + + const LATEST_UPGRADE_ROUTINE = 10; /** * @param string $plugin_file_path @@ -145,7 +174,7 @@ class Amazon_S3_And_CloudFront extends AS3CF_Plugin_Base { * * @throws Exception */ - function __construct( $plugin_file_path, $slug = null ) { + public function __construct( $plugin_file_path, $slug = null ) { $this->plugin_slug = ( is_null( $slug ) ) ? 'amazon-s3-and-cloudfront' : $slug; parent::__construct( $plugin_file_path ); @@ -162,7 +191,7 @@ function __construct( $plugin_file_path, $slug = null ) { * * @throws Exception */ - function init( $plugin_file_path ) { + public function init( $plugin_file_path ) { $this->plugin_title = __( 'Offload Media Lite', 'amazon-s3-and-cloudfront' ); $this->plugin_menu_title = __( 'Offload Media Lite', 'amazon-s3-and-cloudfront' ); @@ -186,8 +215,6 @@ function init( $plugin_file_path ) { Storage::get_provider_key_name() => 'DeliciousBrains\WP_Offload_Media\Providers\Delivery\Storage', ) ); - Media_Library_Item::init_cache(); - $this->set_storage_provider(); $this->set_delivery_provider(); @@ -202,6 +229,8 @@ function init( $plugin_file_path ) { new Upgrade_Filter_Post_Excerpt( $this ); new Upgrade_WPOS3_To_AS3CF( $this ); new Upgrade_Items_Table( $this ); + new Upgrade_Tools_Errors( $this ); + new Upgrade_Item_Extra_Data( $this ); // Plugin setup add_action( 'admin_menu', array( $this, 'admin_menu' ) ); @@ -210,38 +239,18 @@ function init( $plugin_file_path ) { add_filter( 'plugin_action_links', array( $this, 'plugin_actions_settings_link' ), 10, 2 ); add_filter( 'network_admin_plugin_action_links', array( $this, 'plugin_actions_settings_link' ), 10, 2 ); add_filter( 'pre_get_space_used', array( $this, 'multisite_get_space_used' ) ); + // display a notice when either lite or pro is automatically deactivated add_action( 'pre_current_active_plugins', array( $this, 'plugin_deactivated_notice' ) ); add_action( 'as3cf_plugin_load', array( $this, 'remove_access_keys_if_constants_set' ) ); - // Attachment screens/modals - add_action( 'load-upload.php', array( $this, 'load_media_assets' ), 11 ); - add_action( 'admin_enqueue_scripts', array( $this, 'load_attachment_assets' ), 11 ); - add_action( 'add_meta_boxes', array( $this, 'attachment_provider_meta_box' ) ); - // UI AJAX add_action( 'wp_ajax_as3cf-get-buckets', array( $this, 'ajax_get_buckets' ) ); add_action( 'wp_ajax_as3cf-get-url-preview', array( $this, 'ajax_get_url_preview' ) ); - add_action( 'wp_ajax_as3cf_get_attachment_provider_details', array( $this, 'ajax_get_attachment_provider_details' ) ); add_action( 'wp_ajax_as3cf-get-diagnostic-info', array( $this, 'ajax_get_diagnostic_info' ) ); - // Rewriting URLs, doesn't depend on plugin being setup - add_filter( 'wp_get_attachment_url', array( $this, 'wp_get_attachment_url' ), 99, 2 ); - add_filter( 'wp_get_attachment_image_attributes', array( $this, 'wp_get_attachment_image_attributes' ), 99, 3 ); - add_filter( 'get_image_tag', array( $this, 'maybe_encode_get_image_tag' ), 99, 6 ); - add_filter( 'wp_get_attachment_image_src', array( $this, 'maybe_encode_wp_get_attachment_image_src' ), 99, 4 ); - add_filter( 'wp_prepare_attachment_for_js', array( $this, 'maybe_encode_wp_prepare_attachment_for_js', ), 99, 3 ); - add_filter( 'image_get_intermediate_size', array( $this, 'maybe_encode_image_get_intermediate_size' ), 99, 3 ); - add_filter( 'get_attached_file', array( $this, 'get_attached_file' ), 10, 2 ); - add_filter( 'wp_get_original_image_path', array( $this, 'get_attached_file' ), 10, 2 ); - add_filter( 'wp_audio_shortcode', array( $this, 'wp_media_shortcode' ), 100, 5 ); - add_filter( 'wp_video_shortcode', array( $this, 'wp_media_shortcode' ), 100, 5 ); - - // Communication with provider, plugin needs to be setup - add_filter( 'wp_unique_filename', array( $this, 'wp_unique_filename' ), 10, 3 ); - add_filter( 'wp_update_attachment_metadata', array( $this, 'wp_update_attachment_metadata' ), 110, 2 ); - add_filter( 'delete_attachment', array( $this, 'delete_attachment' ), 20 ); - add_filter( 'update_attached_file', array( $this, 'update_attached_file' ), 100, 2 ); + // Enable integrations once everything has been initialized. + add_action( 'as3cf_init', array( $this, 'enable_integrations' ) ); // Listen for settings changes if ( false !== static::settings_constant() ) { @@ -267,6 +276,31 @@ function init( $plugin_file_path ) { $this->register_delivery_provider_assets(); } + /** + * Enable integrations. + * + * @param Amazon_S3_And_CloudFront $as3cf + */ + public function enable_integrations( $as3cf ) { + /** + * Filters which integrations to enable. To disable an integration + * implement this filter and unset all unwanted integrations from + * the array. + * + * @param array $integrations Associative array of integrations + */ + $integrations = apply_filters( 'as3cf_integrations', array( + 'core' => new Core_Integration( $as3cf ), + 'mlib' => new Media_Library_Integration( $as3cf ), + ) ); + + $this->integration_manager = Integration_Manager::get_instance(); + + foreach ( $integrations as $integration_key => $integration ) { + $this->integration_manager->register_integration( $integration_key, $integration ); + } + } + /** * @return Storage_Provider */ @@ -419,6 +453,15 @@ public function get_provider_service_name( $key_name, $type = 'storage' ) { return empty( $class ) ? __( 'Unknown', 'amazon-s3-and-cloudfront' ) : $class::get_provider_service_name(); } + /** + * Getter for the Integrations Manager instance + * + * @return Integration_Manager + */ + public function get_integration_manager() { + return $this->integration_manager; + } + /** * Get the plugin title to be used in page headings * @@ -1047,7 +1090,12 @@ function get_default_object_prefix() { * * @return array */ - function get_allowed_mime_types() { + public function get_allowed_mime_types() { + /** + * Filters list of allowed mime types and file extensions for uploading + * + * @param array $types Mime types keyed by the file extension regex corresponding to those types. + */ return apply_filters( 'as3cf_allowed_mime_types', get_allowed_mime_types() ); } @@ -1125,18 +1173,18 @@ protected function get_local_url_preview( $escape = true, $suffix = 'photo.jpg' * * @return string */ - function get_url_preview( $escape = true, $suffix = 'photo.jpg' ) { + public function get_url_preview( $escape = true, $suffix = 'photo.jpg' ) { $as3cf_item = new Media_Library_Item( $this->get_storage_provider()->get_provider_key_name(), $this->get_setting( 'region' ), $this->get_setting( 'bucket' ), - AS3CF_Utils::trailingslash_prefix( $this->get_file_prefix() ) . $suffix, + AS3CF_Utils::trailingslash_prefix( $this->get_simple_file_prefix() ) . $suffix, false, null, - null + AS3CF_Utils::trailingslash_prefix( $this->get_simple_file_prefix() ) . $suffix ); - $url = $this->get_attachment_provider_url( null, $as3cf_item ); + $url = $as3cf_item->get_provider_url(); if ( is_wp_error( $url ) ) { return ''; @@ -1200,1538 +1248,213 @@ protected function remove_access_keys() { } /** - * Delete bulk objects from an provider bucket - * - * @param string $region - * @param string $bucket - * @param array $objects - * @param bool $log_error - * @param bool $return_on_error - * @param bool $force_new_provider_client if we are deleting in bulk, force new provider client - * to cope with possible different regions + * Get the object versioning string prefix * - * @return bool + * @return string */ - function delete_objects( $region, $bucket, $objects, $log_error = false, $return_on_error = false, $force_new_provider_client = false ) { - $chunks = array_chunk( $objects, 1000 ); + function get_object_version_string() { + if ( $this->get_setting( 'use-yearmonth-folders' ) ) { + $date_format = 'dHis'; + } else { + $date_format = 'YmdHis'; + } - try { - foreach ( $chunks as $chunk ) { - $this->get_provider_client( $region, $force_new_provider_client )->delete_objects( array( - 'Bucket' => $bucket, - 'Objects' => $chunk, - ) ); - } - } catch ( Exception $e ) { - if ( $log_error ) { - AS3CF_Error::log( 'Error removing files from bucket: ' . $e->getMessage() ); - } + // Use current time so that object version is unique + $time = current_time( 'timestamp' ); - return false; - } + $object_version = date( $date_format, $time ) . '/'; + $object_version = apply_filters( 'as3cf_get_object_version_string', $object_version ); - return true; + return $object_version; } /** - * Removes an attachment's files from provider. + * Does file exist * - * @param int $post_id - * @param Media_Library_Item $as3cf_item - * @param bool $include_backups remove previous edited image versions - * @param bool $log_error - * @param bool $return_on_error - * @param bool $force_new_provider_client if we are deleting in bulk, force new provider client - * to cope with possible different regions + * @param string $filename + * @param string $time + * + * @return bool */ - function remove_attachment_files_from_provider( $post_id, Media_Library_Item $as3cf_item, $include_backups = true, $log_error = false, $return_on_error = false, $force_new_provider_client = false ) { - $prefix = $as3cf_item->normalized_path_dir(); - $private_prefix = $as3cf_item->private_prefix(); - $paths = AS3CF_Utils::get_attachment_file_paths( $post_id, false, false, $include_backups ); - $paths = apply_filters( 'as3cf_remove_attachment_paths', $paths, $post_id, $as3cf_item, $include_backups ); - - // If another item in current site shares full size *local* paths, only remove remote files not referenced by duplicates. - // We reference local paths as they should be reflected one way or another remotely, including backups. - $fullsize_paths = AS3CF_Utils::fullsize_paths( $paths ); - $as3cf_items_with_paths = Media_Library_Item::get_by_source_path( $fullsize_paths, array( $post_id ), false ); - - $duplicate_paths = array(); - - foreach ( $as3cf_items_with_paths as $as3cf_item_with_path ) { - /* @var Media_Library_Item $as3cf_item_with_path */ - $duplicate_paths += array_values( AS3CF_Utils::get_attachment_file_paths( $as3cf_item_with_path->source_id(), false, false, $include_backups ) ); - } - - if ( ! empty( $duplicate_paths ) ) { - $paths = array_diff( $paths, $duplicate_paths ); - } - - // Nothing to do, shortcut out. - if ( empty( $paths ) ) { - return; + function does_file_exist( $filename, $time ) { + if ( $this->does_file_exist_local( $filename, $time ) ) { + return true; } - $objects_to_remove = array(); - $paths_to_remove = array_unique( $paths ); - - foreach ( $paths_to_remove as $size => $path ) { - $objects_to_remove[] = array( - 'Key' => $as3cf_item->key( wp_basename( $path ) ), - ); + if ( ! $this->get_setting( 'object-versioning' ) && $this->does_file_exist_provider( $filename, $time ) ) { + return true; } - // finally delete the objects from provider - $this->delete_objects( $as3cf_item->region(), $as3cf_item->bucket(), $objects_to_remove, $log_error, $return_on_error, $force_new_provider_client ); + return false; } /** - * Removes an attachment and intermediate image size files from provider + * Does file exist local + * + * @param string $filename + * @param string $time * - * @param int $post_id - * @param bool $force_new_provider_client if we are deleting in bulk, force new provider client - * to cope with possible different regions + * @return bool */ - function delete_attachment( $post_id, $force_new_provider_client = false ) { - if ( ! $this->is_plugin_setup( true ) ) { - return; - } + function does_file_exist_local( $filename, $time ) { + global $wpdb; - $as3cf_item = Media_Library_Item::get_by_source_id( $post_id ); + $path = wp_upload_dir( $time ); + $path = ltrim( $path['subdir'], '/' ); - if ( ! $as3cf_item ) { - return; + if ( '' !== $path ) { + $path = trailingslashit( $path ); } + $file = $path . $filename; - if ( ! $this->is_attachment_served_by_provider( $post_id, true ) ) { - return; + // WordPress doesn't check its own basic record, so we will. + $sql = $wpdb->prepare( " + SELECT COUNT(*) + FROM $wpdb->postmeta + WHERE meta_key = %s + AND meta_value = %s + ", '_wp_attached_file', $file ); + + if ( (bool) $wpdb->get_var( $sql ) ) { + return true; } - $this->remove_attachment_files_from_provider( $post_id, $as3cf_item, true, true, true, $force_new_provider_client ); + // Check our records of local source path as it also covers original_image. + if ( ! empty( Media_Library_Item::get_by_source_path( array( $file ), array(), true, true ) ) ) { + return true; + } - $as3cf_item->delete(); + return false; } /** - * Handles the upload of the attachment to provider when an attachment is updated using - * the 'wp_update_attachment_metadata' filter + * Does file exist provider * - * @param array $data meta data for attachment - * @param int $post_id + * @param string $filename + * @param string $time * - * @return array + * @return bool * @throws Exception */ - function wp_update_attachment_metadata( $data, $post_id ) { - if ( ! $this->is_plugin_setup( true ) ) { - return $data; - } - - // Protect against updates of partially formed metadata since WordPress 5.3. - // Checks whether new upload currently has no subsizes recorded but is expected to have subsizes during upload, - // and if so, are any of its currently missing sizes part of the set. - if ( ! empty( $data ) && function_exists( 'wp_get_registered_image_subsizes' ) && function_exists( 'wp_get_missing_image_subsizes' ) ) { - - // Plugin compat may require that we wait for wp_generate_attachment_metadata - // to be run before proceeding. I.e Regenrerate Thumbnails requires this - if ( apply_filters( 'as3cf_wait_for_generate_attachment_metadata', false ) ) { - return $data; - } - - if ( empty( $data['sizes'] ) && wp_attachment_is_image( $post_id ) ) { - - // There is no unified way of checking whether subsizes are expected, so we have to duplicate WordPress code here. - $new_sizes = wp_get_registered_image_subsizes(); - $new_sizes = apply_filters( 'intermediate_image_sizes_advanced', $new_sizes, $data, $post_id ); - $missing_sizes = wp_get_missing_image_subsizes( $post_id ); - - if ( ! empty( $new_sizes ) && ! empty( $missing_sizes ) && array_intersect_key( $missing_sizes, $new_sizes ) ) { - return $data; - } - } - } - - $as3cf_item = Media_Library_Item::get_by_source_id( $post_id ); - - if ( ! $as3cf_item && ! $this->get_setting( 'copy-to-s3' ) ) { - // abort if not already uploaded to provider and the copy setting is off - return $data; - } - - if ( empty( $as3cf_item ) ) { - $as3cf_item = null; - } + function does_file_exist_provider( $filename, $time ) { + $bucket = $this->get_setting( 'bucket' ); + $region = $this->get_setting( 'region' ); - // allow provider upload to be cancelled for any reason - $pre = apply_filters( 'as3cf_pre_update_attachment_metadata', false, $data, $post_id, $as3cf_item ); - if ( false !== $pre ) { - return $data; + if ( is_wp_error( $region ) ) { + return false; } - // upload attachment to provider - $attachment_metadata = $this->upload_attachment( $post_id, $data ); - - if ( is_wp_error( $attachment_metadata ) || empty( $attachment_metadata ) || ! is_array( $attachment_metadata ) ) { - return $data; - } + $provider_client = $this->get_provider_client( $region ); + $prefix = AS3CF_Utils::trailingslash_prefix( $this->get_object_prefix() ); + $prefix .= AS3CF_Utils::trailingslash_prefix( $this->get_dynamic_prefix( $time ) ); - return $attachment_metadata; + return $provider_client->does_object_exist( $bucket, $prefix . $filename ); } /** - * Upload attachment to provider + * Generate unique filename * - * @param int $post_id - * @param array|null $data - * @param string|null $file_path - * @param bool $force_new_provider_client if we are uploading in bulk, force new provider client - * to cope with possible different regions - * @param bool $remove_local_files + * @param string $name + * @param string $ext + * @param string $time * - * @return array|Media_Library_Item|WP_Error - * @throws Exception + * @return string */ - public function upload_attachment( $post_id, $data = null, $file_path = null, $force_new_provider_client = false, $remove_local_files = true ) { - static $offloaded_path_filesizes = array(); - static $offloaded_size_paths = array(); - - $return_metadata = null; - if ( is_null( $data ) ) { - $data = wp_get_attachment_metadata( $post_id, true ); - } else { - // As we have passed in the meta, return it later - $return_metadata = $data; - } - - if ( is_wp_error( $data ) ) { - return $data; - } - - // Allow provider upload to be hijacked / cancelled for any reason - try { - $pre = apply_filters( 'as3cf_pre_upload_attachment', false, $post_id, $data ); - } catch ( Exception $e ) { - return $this->return_upload_error( $e->getMessage() ); - } + function generate_unique_filename( $name, $ext, $time ) { + $count = 1; + $filename = $name . '-' . $count . $ext; - if ( false !== $pre ) { - return $data; + while ( $this->does_file_exist( $filename, $time ) ) { + $count++; + $filename = $name . '-' . $count . $ext; } - // If $file_path was passed in with a non-null value, ensure it's a string - if ( ! is_null( $file_path ) && ! is_string( $file_path ) ) { - $error_msg = sprintf( __( 'Media Library item ID %d. Provided path is not a string', 'amazon-s3-and-cloudfront' ), $post_id ); + return $filename; + } - return $this->return_upload_error( $error_msg ); + /** + * Check the plugin is correctly setup + * + * @param bool $with_credentials Do provider credentials need to be set up too? Defaults to false. + * + * @return bool + * + * TODO: Performance - cache / static var by param. + */ + function is_plugin_setup( $with_credentials = false ) { + if ( $with_credentials && $this->get_storage_provider()->needs_access_keys() ) { + // AWS not configured + return false; } - // Ensure WordPress own post meta for relative URL is a string - $attached_file_meta = get_post_meta( $post_id, '_wp_attached_file', true ); - if ( ! is_string( $attached_file_meta ) ) { - $error_msg = sprintf( __( 'Media Library item with ID %d has damaged meta data', 'amazon-s3-and-cloudfront' ), $post_id ); - - return $this->return_upload_error( $error_msg ); + if ( false === (bool) $this->get_setting( 'bucket' ) ) { + // No bucket selected + return false; } - if ( is_null( $file_path ) ) { - $file_path = get_attached_file( $post_id, true ); + if ( is_wp_error( $this->get_setting( 'region' ) ) ) { + // Region error when retrieving bucket location + return false; } - // Check for valid "full" file path before attempting upload - if ( empty( $file_path ) ) { - $error_msg = sprintf( __( 'Media Library item with ID %d does not have a valid file path', 'amazon-s3-and-cloudfront' ), $post_id ); + // All good, let's do this + return true; + } - return $this->return_upload_error( $error_msg, $return_metadata ); + /** + * Return the scheme to be used in URLs + * + * @param bool|null $use_ssl + * + * @return string + */ + function get_url_scheme( $use_ssl = null ) { + if ( $this->use_ssl( $use_ssl ) ) { + $scheme = 'https'; + } else { + $scheme = 'http'; } - $offload_full = true; - $old_item = Media_Library_Item::get_by_source_id( $post_id ); - - // If item not already offloaded, is it a duplicate? - $duplicate = false; - if ( empty( $old_item ) ) { - $old_items = Media_Library_Item::get_by_source_path( $file_path, $post_id, true, true ); - - if ( ! empty( $old_items[0] ) ) { - $duplicate = true; - - /** @var Media_Library_Item $duplicate_item */ - $duplicate_item = $old_items[0]; - - $old_item = new Media_Library_Item( - $duplicate_item->provider(), - $duplicate_item->region(), - $duplicate_item->bucket(), - $duplicate_item->path(), - $duplicate_item->is_private(), - $post_id, - $duplicate_item->source_path(), - wp_basename( $duplicate_item->original_source_path() ), - $duplicate_item->extra_info() - ); - - $old_item->save(); - - // If original offloaded in same process, skip offloading anything it's already processed. - // Otherwise, do not need to offload full file if duplicate and file missing. - if ( ! empty( $offloaded_path_filesizes[ $duplicate_item->id() ] ) ) { - $offloaded_path_filesizes[ $old_item->id() ] = $offloaded_path_filesizes[ $duplicate_item->id() ]; - $offloaded_size_paths[ $old_item->id() ] = $offloaded_size_paths[ $duplicate_item->id() ]; - } elseif ( ! file_exists( $file_path ) ) { - $offload_full = false; - } + return $scheme; + } - unset( $old_items, $duplicate_item ); - } + /** + * Determine when to use https in URLS + * + * @param bool|null $use_ssl + * + * @return bool + */ + public function use_ssl( $use_ssl = null ) { + if ( is_ssl() ) { + $use_ssl = true; } - // If not already offloaded in request, check full file exists locally before attempting offload. - if ( $offload_full ) { - if ( $old_item && ! empty( $offloaded_path_filesizes[ $old_item->id() ][ $file_path ] ) ) { - $offload_full = false; - } elseif ( ! file_exists( $file_path ) ) { - $error_msg = sprintf( __( 'File %s does not exist', 'amazon-s3-and-cloudfront' ), $file_path ); - - return $this->return_upload_error( $error_msg, $return_metadata ); - } + if ( ! is_bool( $use_ssl ) ) { + $use_ssl = $this->get_setting( 'force-https' ); } - $file_paths = AS3CF_Utils::get_attachment_file_paths( $post_id, false, $data ); - $file_paths = array_diff( $file_paths, array( $file_path ) ); - - // Are there any files not already offloaded if full already offloaded in this request? - if ( false === $offload_full ) { - if ( empty( $file_paths ) ) { - // Item does not have any additional files, we're done. - return $return_metadata; - } - - if ( ! empty( $offloaded_size_paths[ $old_item->id() ] ) && empty( array_diff_key( $file_paths, $offloaded_size_paths[ $old_item->id() ] ) ) ) { - // Item's additional files all offloaded, we're done. - return $return_metadata; - } + if ( empty( $use_ssl ) ) { + $use_ssl = false; } - // Get original file's stats. - $file_name = wp_basename( $file_path ); - $type = get_post_mime_type( $post_id ); - $allowed_types = $this->get_allowed_mime_types(); - - // Check mime type of file is in allowed provider mime types. - // Note: This check is based on the item's original upload format. - if ( ! in_array( $type, $allowed_types ) ) { - $error_msg = sprintf( __( 'Mime type %s is not allowed', 'amazon-s3-and-cloudfront' ), $type ); + return apply_filters( 'as3cf_use_ssl', $use_ssl ); + } - return $this->return_upload_error( $error_msg, $return_metadata ); + /** + * Get the custom object prefix if enabled + * + * @param string $toggle_setting + * + * @return string + */ + function get_object_prefix( $toggle_setting = 'enable-object-prefix' ) { + if ( $this->get_setting( $toggle_setting ) ) { + return trailingslashit( trim( $this->get_setting( 'object-prefix' ) ) ); } - $acl = $this->get_storage_provider()->get_default_acl(); - - // check the attachment already exists in provider, eg. edit or restore image - if ( $old_item ) { - // Must be offloaded to same provider as currently configured. - if ( ! $this->is_attachment_served_by_provider( $post_id, true ) ) { - return $this->return_upload_error( __( 'Already offloaded to a different provider', 'amazon-s3-and-cloudfront' ), $return_metadata ); - } - - // Use private ACL if existing offload is already private. - if ( $old_item->is_private() ) { - $acl = $this->get_storage_provider()->get_private_acl(); - } - - // use existing prefix - $prefix = $old_item->normalized_path_dir(); - // use existing private prefix - $private_prefix = $old_item->private_prefix(); - // use existing bucket - $bucket = $old_item->bucket(); - // get existing region - $region = $old_item->region(); - // Get existing original filename. - $original_filename = wp_basename( $old_item->original_source_path() ); - } else { - // derive prefix from various settings - $prefix = $this->get_new_attachment_prefix( $post_id, $data ); - - // maybe set a private prefix. - if ( $this->private_prefix_enabled() ) { - $private_prefix = AS3CF_Utils::trailingslash_prefix( $this->get_setting( 'signed-urls-object-prefix', '' ) ); - } else { - $private_prefix = ''; - } - - // use bucket from settings - $bucket = $this->get_setting( 'bucket' ); - $region = $this->get_setting( 'region' ); - if ( is_wp_error( $region ) ) { - $region = ''; - } - - // There may be an original image that can override the default original filename. - $original_filename = empty( $data['original_image'] ) ? null : $data['original_image']; - } - - $acl = apply_filters( 'wps3_upload_acl', $acl, $type, $data, $post_id, $this ); // Old naming convention, will be deprecated soon - $acl = apply_filters( 'as3cf_upload_acl', $acl, $data, $post_id ); - $is_private = ! empty( $acl ) && $this->get_storage_provider()->get_private_acl() === $acl; - - $args = array( - 'Bucket' => $bucket, - 'Key' => $prefix . $file_name, - 'SourceFile' => $file_path, - 'ContentType' => $this->get_mime_type( $file_path ), - 'CacheControl' => 'max-age=31536000', - 'Expires' => date( 'D, d M Y H:i:s O', time() + 31536000 ), - ); - - $image_size = wp_attachment_is_image( $post_id ) ? 'full' : ''; - - // Only set ACL if actually required, some storage provider and bucket settings disable changing ACL. - if ( ! empty( $acl ) && $this->use_acl_for_intermediate_size( $post_id, $image_size, $bucket ) ) { - $args['ACL'] = $acl; - } - - // TODO: Remove GZIP functionality. - // Handle gzip on supported items - if ( $this->should_gzip_file( $file_path, $type ) && false !== ( $gzip_body = gzencode( file_get_contents( $file_path ) ) ) ) { - unset( $args['SourceFile'] ); - - $args['Body'] = $gzip_body; - $args['ContentEncoding'] = 'gzip'; - } - - $args = apply_filters( 'as3cf_object_meta', $args, $post_id, $image_size, false ); - - $provider = $this->get_storage_provider()->get_provider_key_name(); - $region = $bucket !== $args['Bucket'] ? $this->get_bucket_region( $args['Bucket'], true ) : $region; - $is_private = ( ! empty( $args['ACL'] ) && $this->get_storage_provider()->get_private_acl() === $args['ACL'] ) ? true : $is_private; - $extra_info = empty( $old_item ) ? array( 'private_prefix' => $private_prefix ) : $old_item->extra_info(); - $item_id = empty( $old_item ) ? null : $old_item->id(); - - // Protect against filter use and only set ACL if actually required, some storage provider and bucket settings disable changing ACL. - if ( isset( $args['ACL'] ) && ! $this->use_acl_for_intermediate_size( $post_id, $image_size, $bucket ) ) { - unset( $args['ACL'] ); - } - - $as3cf_item = new Media_Library_Item( $provider, $region, $args['Bucket'], $args['Key'], $is_private, $post_id, $file_path, $original_filename, $extra_info, $item_id ); - - // With public path and private prefix now in place, we can set the final path for the full sized file. - $size_private_prefix = $as3cf_item->is_private() ? $as3cf_item->private_prefix() : ''; - $args['Key'] = $size_private_prefix . $args['Key']; - - do_action( 'as3cf_upload_attachment_pre_remove', $post_id, $as3cf_item, $as3cf_item->normalized_path_dir(), $args ); - - $new_offload_path_filesizes = array(); - $new_offload_size_paths = array(); - $files_to_remove = array(); - - $provider_client = $this->get_provider_client( $as3cf_item->region(), $force_new_provider_client ); - - if ( $offload_full ) { - try { - // May raise exception, so don't offload anything else if there's an error. - $filesize = (int) filesize( $file_path ); - - // May raise exception, so don't offload anything else if there's an error. - $provider_client->upload_object( $args ); - - $new_offload_path_filesizes[ $file_path ] = $filesize; // Note: pre `as3cf_object_meta` filter value. - $new_offload_size_paths['original'] = $file_path; - $files_to_remove[] = $file_path; // Note: pre `as3cf_object_meta` filter value. - } catch ( Exception $e ) { - $error_msg = sprintf( __( 'Error offloading %s to provider: %s', 'amazon-s3-and-cloudfront' ), $file_path, $e->getMessage() ); - - return $this->return_upload_error( $error_msg, $return_metadata ); - } - } - - $additional_images = array(); - $private_sizes = array(); // Reset private sizes to be as expected at time of (re)upload. - - foreach ( $file_paths as $size => $file_path ) { - if ( ! in_array( $file_path, $files_to_remove ) ) { - $acl = apply_filters( 'as3cf_upload_acl_sizes', $this->get_storage_provider()->get_default_acl(), $size, $post_id, $data ); - - if ( ! empty( $acl ) && $this->get_storage_provider()->get_private_acl() === $acl ) { - $private_sizes[] = $size; - } - - // Public path, modified to private in next block as needed. - $additional_images[ $size ] = array( - 'Key' => $as3cf_item->normalized_path_dir() . wp_basename( $file_path ), - 'SourceFile' => $file_path, - 'ContentType' => $this->get_mime_type( $file_path ), - ); - - // Only set ACL if actually required, some storage provider and bucket settings disable changing ACL. - if ( ! empty( $acl ) && $this->use_acl_for_intermediate_size( $post_id, $size, $bucket, $as3cf_item ) ) { - $additional_images[ $size ]['ACL'] = $acl; - } - } else { - // If the previous offload of file path was private, this size also needs to be private. - // This is a case of first (in process) offload wins, duplicate file paths should have same access. - if ( ! empty( $private_sizes ) ) { - $duplicate_private = array_intersect( $private_sizes, array_keys( array_intersect( $file_paths, array( $file_path ) ) ) ); - - if ( ! empty( $duplicate_private ) ) { - $private_sizes[] = $size; - } - } - } - } - - $upload_errors = array(); - - foreach ( $additional_images as $size => $image ) { - // If this file has already been offloaded during this request, skip actual offload. - if ( $old_item && ! empty( $offloaded_path_filesizes[ $old_item->id() ][ $image['SourceFile'] ] ) ) { - // We still have to record whether this size is private based on previous offload of the source file. - // We also need to ensure this file is marked as possibly needing removal from server, and size has been processed. - if ( ! empty( $private_sizes ) ) { - $duplicate_private = array_intersect( $private_sizes, array_keys( array_intersect( $file_paths, array( $image['SourceFile'] ) ) ) ); - - if ( ! empty( $duplicate_private ) ) { - $private_sizes[] = $size; - } - } - - // Processed file, but not this duplicate size, so file may have been re-created by WordPress. - if ( file_exists( $image['SourceFile'] ) ) { - $files_to_remove[] = $image['SourceFile']; - } - $new_offload_size_paths[ $size ] = $image['SourceFile']; - continue; - } - - $args = apply_filters( 'as3cf_object_meta', array_merge( $args, $image ), $post_id, $size, false ); - - // Is size private and therefore needs to be in private prefix? - $size_private_prefix = in_array( $size, $private_sizes ) ? $as3cf_item->private_prefix() : ''; - $args['Key'] = $size_private_prefix . $args['Key']; - - // Protect against filter use and only set ACL if actually required, some storage provider and bucket settings disable changing ACL. - if ( isset( $args['ACL'] ) && ! $this->use_acl_for_intermediate_size( $post_id, $size, $bucket, $as3cf_item ) ) { - unset( $args['ACL'] ); - } - - if ( ! file_exists( $args['SourceFile'] ) ) { - if ( ! $duplicate ) { - $upload_errors[] = $this->return_upload_error( sprintf( __( 'File %s does not exist', 'amazon-s3-and-cloudfront' ), $args['SourceFile'] ) ); - } - continue; - } - - try { - // May raise exception, but for sizes we'll just log it and maybe try again later if called. - $provider_client->upload_object( $args ); - $files_to_remove[] = $image['SourceFile']; // Note: pre `as3cf_object_meta` filter value. - $new_offload_size_paths[ $size ] = $image['SourceFile']; - - // May raise exception, we'll log that, and carry on anyway. - $new_offload_path_filesizes[ $image['SourceFile'] ] = (int) filesize( $image['SourceFile'] ); // Note: pre `as3cf_object_meta` filter value. - } catch ( Exception $e ) { - $upload_errors[] = $this->return_upload_error( sprintf( __( 'Error offloading %s to provider: %s', 'amazon-s3-and-cloudfront' ), $args['SourceFile'], $e->getMessage() ) ); - } - - // Edge Case: If previously uploaded and a different original_image wasn't picked up but is now, record it. - // This is most likely to happen if older version of plugin was used with WP5.3 and large or rotated image auto-created. - if ( 'original_image' === $size && wp_basename( $as3cf_item->original_source_path() ) !== wp_basename( $image['SourceFile'] ) ) { - $as3cf_item = new Media_Library_Item( - $as3cf_item->provider(), - $as3cf_item->region(), - $as3cf_item->bucket(), - $as3cf_item->path(), - $as3cf_item->is_private(), - $as3cf_item->source_id(), - $as3cf_item->source_path(), - wp_basename( $image['SourceFile'] ), - $as3cf_item->extra_info(), - $as3cf_item->id() - ); - } - } - - $remove_local_files_setting = $this->get_setting( 'remove-local-file' ); - - if ( $remove_local_files && $remove_local_files_setting ) { - // Allow other functions to remove files after they have processed - $files_to_remove = apply_filters( 'as3cf_upload_attachment_local_files_to_remove', $files_to_remove, $post_id, $file_path ); - - // Remove duplicates - $files_to_remove = array_unique( $files_to_remove ); - - $filesize_total = 0; - if ( ! empty( $old_item ) && ! empty( $offloaded_path_filesizes[ $old_item->id() ] ) ) { - $filesize_total = array_sum( $offloaded_path_filesizes[ $old_item->id() ] ); - } - // Delete the files and record original file's size before removal. - $this->remove_local_files( $files_to_remove, $post_id, $filesize_total ); - - // Store filesize in the attachment meta data for use by WP if we've just offloaded the full size file. - if ( ! empty( $filesize ) ) { - $data['filesize'] = $filesize; - - if ( is_null( $return_metadata ) ) { - // Update metadata with filesize - update_post_meta( $post_id, '_wp_attachment_metadata', $data ); - } - } - } - - // Make sure we don't have cached file sizes in the meta if we previously added it. - if ( ! $remove_local_files_setting && isset( $data['filesize'] ) && ! empty( get_post_meta( $post_id, 'as3cf_filesize_total', true ) ) ) { - $data = $this->maybe_cleanup_filesize_metadata( $post_id, $data, empty( $return_metadata ) ); - } - - // Additional image sizes have custom ACLs, record them. - if ( ! empty( $private_sizes ) ) { - $extra_info = $as3cf_item->extra_info(); - $extra_info['private_sizes'] = $private_sizes; - - $as3cf_item = new Media_Library_Item( - $as3cf_item->provider(), - $as3cf_item->region(), - $as3cf_item->bucket(), - $as3cf_item->path(), - $as3cf_item->is_private(), - $as3cf_item->source_id(), - $as3cf_item->source_path(), - wp_basename( $as3cf_item->original_source_path() ), - $extra_info, - $as3cf_item->id() - ); - } - - // All done, save record of offloaded item. - $as3cf_item->save(); - - // Keep track of individual files offloaded during this request. - if ( empty( $offloaded_path_filesizes[ $as3cf_item->id() ] ) ) { - $offloaded_path_filesizes[ $as3cf_item->id() ] = $new_offload_path_filesizes; - $offloaded_size_paths[ $as3cf_item->id() ] = $new_offload_size_paths; - } else { - $offloaded_path_filesizes[ $as3cf_item->id() ] += $new_offload_path_filesizes; - $offloaded_size_paths[ $as3cf_item->id() ] += $new_offload_size_paths; - } - - // Keep track of attachments uploaded by this instance. - $this->uploaded_post_ids[] = $post_id; - - do_action( 'as3cf_post_upload_attachment', $post_id, $as3cf_item ); - - if ( $upload_errors ) { - return $this->consolidate_upload_errors( $upload_errors ); - } - - if ( ! is_null( $return_metadata ) ) { - // If the attachment metadata is supplied, return it - return $data; - } - - return $as3cf_item; - } - - /** - * Get a file's real mime type - * - * @param string $file_path - * - * @return string - */ - protected function get_mime_type( $file_path ) { - $file_type = wp_check_filetype_and_ext( $file_path, wp_basename( $file_path ) ); - - return $file_type['type']; - } - - /** - * Should gzip file - * - * @param string $file_path - * @param string $type - * - * @return bool - */ - protected function should_gzip_file( $file_path, $type ) { - $mimes = $this->get_mime_types_to_gzip( true ); - - if ( in_array( $type, $mimes ) && is_readable( $file_path ) ) { - return true; - } - - return false; - } - - /** - * Get mime types to gzip - * - * @param bool $media_library - * - * @return array - */ - protected function get_mime_types_to_gzip( $media_library = false ) { - $mimes = apply_filters( 'as3cf_gzip_mime_types', array( - 'css' => 'text/css', - 'eot' => 'application/vnd.ms-fontobject', - 'html' => 'text/html', - 'ico' => 'image/x-icon', - 'js' => 'application/javascript', - 'json' => 'application/json', - 'otf' => 'application/x-font-opentype', - 'rss' => 'application/rss+xml', - 'svg' => 'image/svg+xml', - 'ttf' => 'application/x-font-ttf', - 'woff' => 'application/font-woff', - 'woff2' => 'application/font-woff2', - 'xml' => 'application/xml', - ), $media_library ); - - return $mimes; - } - - /** - * Helper to record errors and return meta data on upload error. - * - * @param string $error_msg - * @param array|null $return - * - * @return array|WP_Error - */ - protected function return_upload_error( $error_msg, $return = null ) { - - AS3CF_Error::log( $error_msg ); - - if ( is_null( $return ) ) { - return new WP_Error( 'exception', $error_msg ); - } - - return $return; - } - - /** - * Remove files from the local site, recording total filesize in meta if attachment ID given. - * - * @param array $file_paths Files to remove. - * @param int $attachment_id Optional, if supplied filesize metadata recorded. - * @param int $filesize_total Optional, if removing partial set of an attachment's files, pass in previously removed total. - */ - function remove_local_files( $file_paths, $attachment_id = 0, $filesize_total = 0 ) { - if ( empty( $filesize_total ) ) { - $filesize_total = 0; - } - - foreach ( $file_paths as $index => $path ) { - if ( ! empty( $attachment_id ) && is_int( $attachment_id ) ) { - $bytes = filesize( $path ); - - $filesize_total += ( false !== $bytes ) ? $bytes : 0; - } - - // Individual files might still be kept local, but we're still going to count them towards total above. - if ( false !== ( $pre = apply_filters( 'as3cf_preserve_file_from_local_removal', false, $path ) ) ) { - continue; - } - - if ( ! @unlink( $path ) ) { - $message = 'Error removing local file '; - - if ( ! file_exists( $path ) ) { - $message = "Error removing local file. Couldn't find the file at "; - } else if ( ! is_writable( $path ) ) { - $message = 'Error removing local file. Ownership or permissions are mis-configured for '; - } - - AS3CF_Error::log( $message . $path ); - } - } - - // If we were able to sum up file sizes for an attachment, record it. - if ( ! empty( $attachment_id ) && is_int( $attachment_id ) && $filesize_total > 0 ) { - update_post_meta( $attachment_id, 'as3cf_filesize_total', $filesize_total ); - } - } - - /** - * Get the object versioning string prefix - * - * @return string - */ - function get_object_version_string() { - if ( $this->get_setting( 'use-yearmonth-folders' ) ) { - $date_format = 'dHis'; - } else { - $date_format = 'YmdHis'; - } - - // Use current time so that object version is unique - $time = current_time( 'timestamp' ); - - $object_version = date( $date_format, $time ) . '/'; - $object_version = apply_filters( 'as3cf_get_object_version_string', $object_version ); - - return $object_version; - } - - /** - * Get the upload folder time from given URL - * - * @param string $url - * - * @return null|string YYYY/MM format. - */ - function get_folder_time_from_url( $url ) { - if ( ! is_string( $url ) ) { - return null; - } - - preg_match( '@[0-9]{4}/[0-9]{2}/@', $url, $matches ); - - if ( isset( $matches[0] ) ) { - return untrailingslashit( $matches[0] ); - } - - return null; - } - - /** - * Get the year/month string for attachment's upload. - * - * Fall back to post date if attached, otherwise current date. - * - * @param int $post_id Attachment's ID - * @param array $data Attachment's metadata - * - * @return string - */ - function get_attachment_folder_year_month( $post_id, $data = null ) { - if ( isset( $data['file'] ) ) { - $time = $this->get_folder_time_from_url( $data['file'] ); - } - - if ( empty( $time ) && ( $local_url = wp_get_attachment_url( $post_id ) ) ) { - $time = $this->get_folder_time_from_url( $local_url ); - } - - if ( empty( $time ) ) { - $time = date( 'Y/m' ); - - if ( ! ( $attach = get_post( $post_id ) ) ) { - return $time; - } - - if ( ! $attach->post_parent ) { - return $time; - } - - if ( ! ( $post = get_post( $attach->post_parent ) ) ) { - return $time; - } - - if ( substr( $post->post_date_gmt, 0, 4 ) > 0 ) { - return date( 'Y/m', strtotime( $post->post_date_gmt . ' +0000' ) ); - } - } - - return $time; - } - - /** - * Filters the result when generating a unique file name. - * - * @param string $filename Unique file name. - * @param string $ext File extension, eg. ".png". - * @param string $dir Directory path. - * - * @return string - * @since 4.5.0 - * - */ - public function wp_unique_filename( $filename, $ext, $dir ) { - // Get Post ID if uploaded in post screen. - $post_id = filter_input( INPUT_POST, 'post_id', FILTER_VALIDATE_INT ); - - $filename = $this->filter_unique_filename( $filename, $ext, $dir, $post_id ); - - return $filename; - } - - /** - * Create unique names for file to be uploaded to AWS. - * This only applies when the remove local file option is enabled. - * - * @param string $filename Unique file name. - * @param string $ext File extension, eg. ".png". - * @param string $dir Directory path. - * @param int $post_id Attachment's parent Post ID. - * - * @return string - */ - public function filter_unique_filename( $filename, $ext, $dir, $post_id = null ) { - if ( ! $this->is_plugin_setup( true ) ) { - return $filename; - } - - // sanitize the file name before we begin processing - $filename = sanitize_file_name( $filename ); - $ext = strtolower( $ext ); - $name = wp_basename( $filename, $ext ); - - // Edge case: if file is named '.ext', treat as an empty name. - if ( $name === $ext ) { - $name = ''; - } - - // Rebuild filename with lowercase extension as provider will have converted extension on upload. - $filename = $name . $ext; - $time = current_time( 'mysql' ); - - // Get time if uploaded in post screen. - if ( ! empty( $post_id ) ) { - $time = $this->get_post_time( $post_id ); - } - - if ( ! $this->does_file_exist( $filename, $time ) ) { - // File doesn't exist locally or on provider, return it. - return $filename; - } - - $filename = $this->generate_unique_filename( $name, $ext, $time ); - - return $filename; - } - - /** - * Get post time - * - * @param int $post_id - * - * @return string - */ - function get_post_time( $post_id ) { - $time = current_time( 'mysql' ); - - if ( ! $post = get_post( $post_id ) ) { - return $time; - } - - if ( substr( $post->post_date, 0, 4 ) > 0 ) { - $time = $post->post_date; - } - - return $time; - } - - /** - * Does file exist - * - * @param string $filename - * @param string $time - * - * @return bool - */ - function does_file_exist( $filename, $time ) { - if ( $this->does_file_exist_local( $filename, $time ) ) { - return true; - } - - if ( ! $this->get_setting( 'object-versioning' ) && $this->does_file_exist_provider( $filename, $time ) ) { - return true; - } - - return false; - } - - /** - * Does file exist local - * - * @param string $filename - * @param string $time - * - * @return bool - */ - function does_file_exist_local( $filename, $time ) { - global $wpdb; - - $path = wp_upload_dir( $time ); - $path = ltrim( $path['subdir'], '/' ); - - if ( '' !== $path ) { - $path = trailingslashit( $path ); - } - $file = $path . $filename; - - // WordPress doesn't check its own basic record, so we will. - $sql = $wpdb->prepare( " - SELECT COUNT(*) - FROM $wpdb->postmeta - WHERE meta_key = %s - AND meta_value = %s - ", '_wp_attached_file', $file ); - - if ( (bool) $wpdb->get_var( $sql ) ) { - return true; - } - - // Check our records of local source path as it also covers original_image. - if ( ! empty( Media_Library_Item::get_by_source_path( array( $file ), array(), true, true ) ) ) { - return true; - } - - return false; - } - - /** - * Does file exist provider - * - * @param string $filename - * @param string $time - * - * @return bool - * @throws Exception - */ - function does_file_exist_provider( $filename, $time ) { - $bucket = $this->get_setting( 'bucket' ); - $region = $this->get_setting( 'region' ); - - if ( is_wp_error( $region ) ) { - return false; - } - - $provider_client = $this->get_provider_client( $region ); - $prefix = AS3CF_Utils::trailingslash_prefix( $this->get_object_prefix() ); - $prefix .= AS3CF_Utils::trailingslash_prefix( $this->get_dynamic_prefix( $time ) ); - - return $provider_client->does_object_exist( $bucket, $prefix . $filename ); - } - - /** - * Generate unique filename - * - * @param string $name - * @param string $ext - * @param string $time - * - * @return string - */ - function generate_unique_filename( $name, $ext, $time ) { - $count = 1; - $filename = $name . '-' . $count . $ext; - - while ( $this->does_file_exist( $filename, $time ) ) { - $count++; - $filename = $name . '-' . $count . $ext; - } - - return $filename; - } - - /** - * Check the plugin is correctly setup - * - * @param bool $with_credentials Do provider credentials need to be set up too? Defaults to false. - * - * @return bool - * - * TODO: Performance - cache / static var by param. - */ - function is_plugin_setup( $with_credentials = false ) { - if ( $with_credentials && $this->get_storage_provider()->needs_access_keys() ) { - // AWS not configured - return false; - } - - if ( false === (bool) $this->get_setting( 'bucket' ) ) { - // No bucket selected - return false; - } - - if ( is_wp_error( $this->get_setting( 'region' ) ) ) { - // Region error when retrieving bucket location - return false; - } - - // All good, let's do this - return true; - } - - /** - * Generate a link to download a file from Amazon provider using query string - * authentication. This link is only valid for a limited amount of time. - * - * @param int $post_id Post ID of the attachment - * @param int|null $expires Seconds for the link to live - * @param string|null $size Size of the image to get - * @param array $headers Header overrides for request - * @param bool $skip_rewrite_check - * - * @return string|bool|WP_Error - */ - public function get_secure_attachment_url( $post_id, $expires = null, $size = null, $headers = array(), $skip_rewrite_check = false ) { - if ( is_null( $expires ) ) { - $expires = self::DEFAULT_EXPIRES; - } - - return $this->get_attachment_url( $post_id, $expires, $size, null, $headers, $skip_rewrite_check ); - } - - /** - * Return the scheme to be used in URLs - * - * @param bool|null $use_ssl - * - * @return string - */ - function get_url_scheme( $use_ssl = null ) { - if ( $this->use_ssl( $use_ssl ) ) { - $scheme = 'https'; - } else { - $scheme = 'http'; - } - - return $scheme; - } - - /** - * Determine when to use https in URLS - * - * @param bool|null $use_ssl - * - * @return bool - */ - public function use_ssl( $use_ssl = null ) { - if ( is_ssl() ) { - $use_ssl = true; - } - - if ( ! is_bool( $use_ssl ) ) { - $use_ssl = $this->get_setting( 'force-https' ); - } - - if ( empty( $use_ssl ) ) { - $use_ssl = false; - } - - return apply_filters( 'as3cf_use_ssl', $use_ssl ); - } - - /** - * Get the custom object prefix if enabled - * - * @param string $toggle_setting - * - * @return string - */ - function get_object_prefix( $toggle_setting = 'enable-object-prefix' ) { - if ( $this->get_setting( $toggle_setting ) ) { - return trailingslashit( trim( $this->get_setting( 'object-prefix' ) ) ); - } - - return ''; - } - - /** - * Get the file prefix - * - * @param null|string $time - * @param bool $object_versioning_allowed Can an Object Versioning string be appended if setting turned on? Default true. - * - * @return string - */ - public function get_file_prefix( $time = null, $object_versioning_allowed = true ) { - $prefix = AS3CF_Utils::trailingslash_prefix( $this->get_object_prefix() ); - $prefix .= AS3CF_Utils::trailingslash_prefix( $this->get_dynamic_prefix( $time ) ); - - if ( ! empty( $object_versioning_allowed ) && $this->get_setting( 'object-versioning' ) ) { - $prefix .= AS3CF_Utils::trailingslash_prefix( $this->get_object_version_string() ); - } - - return $prefix; - } - - /** - * Get attachment's new public prefix path for current settings. - * - * @param int $post_id Attachment ID - * @param array $metadata Optional attachment metadata - * @param bool $object_versioning_allowed Can an Object Versioning string be appended if setting turned on? Default true. - * - * @return string - */ - public function get_new_attachment_prefix( $post_id, $metadata = null, $object_versioning_allowed = true ) { - if ( empty( $metadata ) ) { - $metadata = wp_get_attachment_metadata( $post_id, true ); - } - - $time = $this->get_attachment_folder_year_month( $post_id, $metadata ); - - return $this->get_file_prefix( $time, $object_versioning_allowed ); - } - - /** - * Get the url of the file from provider - * - * @param int $post_id Post ID of the attachment, required. - * @param int|null $expires Seconds for the link to live, optional. - * @param string|null $size Size of the image to get, optional. - * @param array|null $meta Pre retrieved _wp_attachment_metadata for the attachment, optional. - * @param array $headers Header overrides for request, optional. - * @param bool $skip_rewrite_check Always return the URL regardless of the 'Rewrite File URLs' setting, optional, default: false. - * Useful for the EDD and Woo addons to not break download URLs when the option is disabled. - * - * @return string|bool|WP_Error - */ - public function get_attachment_url( $post_id, $expires = null, $size = null, $meta = null, $headers = array(), $skip_rewrite_check = false ) { - if ( ! ( $as3cf_item = $this->is_attachment_served_by_provider( $post_id, $skip_rewrite_check ) ) ) { - return false; - } - - $url = $this->get_attachment_provider_url( $post_id, $as3cf_item, $expires, $size, $meta, $headers ); - - return apply_filters( 'as3cf_wp_get_attachment_url', $url, $post_id ); - } - - /** - * Get attachment local URL. - * - * This is a direct copy of wp_get_attachment_url() from /wp-includes/post.php - * as we filter the URL in AS3CF and can't remove this filter using the current implementation - * of globals for class instances. - * - * @param int $post_id - * - * @return string|false Attachment URL, otherwise false. - */ - public function get_attachment_local_url( $post_id ) { - $url = ''; - - // Get attached file. - if ( $file = get_post_meta( $post_id, '_wp_attached_file', true ) ) { - // Get upload directory. - if ( ( $uploads = wp_upload_dir() ) && false === $uploads['error'] ) { - // Check that the upload base exists in the file location. - if ( 0 === strpos( $file, $uploads['basedir'] ) ) { - // Replace file location with url location. - $url = str_replace( $uploads['basedir'], $uploads['baseurl'], $file ); - } elseif ( false !== strpos( $file, 'wp-content/uploads' ) ) { - $url = $uploads['baseurl'] . substr( $file, strpos( $file, 'wp-content/uploads' ) + 18 ); - } else { - // It's a newly-uploaded file, therefore $file is relative to the basedir. - $url = $uploads['baseurl'] . "/$file"; - } - } - } - - if ( empty( $url ) ) { - return false; - } - - $url = $this->maybe_fix_local_subsite_url( $url ); - - return $url; - } - - /** - * Get attachment local URL size. - * - * @param int $post_id - * @param string|null $size - * - * @return false|string - */ - public function get_attachment_local_url_size( $post_id, $size = null ) { - $url = $this->get_attachment_local_url( $post_id ); - - if ( empty( $size ) ) { - return $url; - } - - $meta = get_post_meta( $post_id, '_wp_attachment_metadata', true ); - - if ( empty( $meta['sizes'][ $size ]['file'] ) ) { - // No alternative sizes available, return - return $url; - } - - return str_replace( wp_basename( $url ), $meta['sizes'][ $size ]['file'], $url ); - } - - /** - * Get the provider URL for an attachment - * - * @param int $post_id - * @param Media_Library_Item $as3cf_item - * @param null|int $expires - * @param null|string|array $size - * @param null|array $meta - * @param array $headers - * - * @return string|WP_Error - */ - public function get_attachment_provider_url( $post_id, Media_Library_Item $as3cf_item, $expires = null, $size = null, $meta = null, $headers = array() ) { - $size = AS3CF_Utils::maybe_convert_size_to_string( $post_id, $size ); - - // Is a signed expiring URL required for the requested object? - if ( is_null( $expires ) ) { - $expires = $as3cf_item->is_private_size( $size ) ? self::DEFAULT_EXPIRES : null; - } else { - $expires = $as3cf_item->is_private_size( $size ) ? $expires : null; - } - - $item_path = $as3cf_item->path(); - - if ( ! is_null( $size ) ) { - if ( is_null( $meta ) ) { - $meta = get_post_meta( $post_id, '_wp_attachment_metadata', true ); - } - - if ( is_wp_error( $meta ) ) { - return $meta; - } - - if ( ! empty( $meta ) && isset( $meta['sizes'][ $size ]['file'] ) ) { - $size_prefix = dirname( $item_path ); - $size_file_prefix = ( '.' === $size_prefix ) ? '' : $size_prefix . '/'; - - $item_path = $size_file_prefix . $meta['sizes'][ $size ]['file']; - } - } - - $scheme = $this->get_url_scheme(); - $enable_delivery_domain = $this->get_delivery_provider()->delivery_domain_allowed() ? $this->get_setting( 'enable-delivery-domain' ) : false; - $delivery_domain = $this->get_setting( 'delivery-domain' ); - - if ( ! $enable_delivery_domain || empty( $delivery_domain ) ) { - $region = $as3cf_item->region(); - - if ( is_wp_error( $region ) ) { - return $region; - } - - $delivery_domain = $this->get_storage_provider()->get_url_domain( $as3cf_item->bucket(), $region, $expires ); - } else { - $delivery_domain = AS3CF_Utils::sanitize_custom_domain( $delivery_domain ); - } - - if ( ! is_null( $expires ) && $this->is_plugin_setup( true ) ) { - try { - $timestamp = time() + apply_filters( 'as3cf_expires', $expires ); - $url = $this->get_delivery_provider()->get_signed_url( $as3cf_item, $item_path, $delivery_domain, $scheme, $timestamp, $headers ); - - return apply_filters( 'as3cf_get_attachment_secure_url', $url, $as3cf_item, $post_id, $timestamp, $headers ); - } catch ( Exception $e ) { - return new WP_Error( 'exception', $e->getMessage() ); - } - } else { - try { - $url = $this->get_delivery_provider()->get_url( $as3cf_item, $item_path, $delivery_domain, $scheme, $headers ); - - return apply_filters( 'as3cf_get_attachment_url', $url, $as3cf_item, $post_id, $expires, $headers ); - } catch ( Exception $e ) { - return new WP_Error( 'exception', $e->getMessage() ); - } - } - } - - /** - * Get attachment url - * - * @param string $url - * @param int $post_id - * - * @return bool|mixed|WP_Error - */ - public function wp_get_attachment_url( $url, $post_id ) { - if ( $this->plugin_compat->is_customizer_crop_action() ) { - return $url; - } - - $new_url = $this->get_attachment_url( $post_id ); - - if ( false === $new_url ) { - return $url; - } - - $new_url = apply_filters( 'wps3_get_attachment_url', $new_url, $post_id, $this ); // Old naming convention, will be deprecated soon - $new_url = apply_filters( 'as3cf_wp_get_attachment_url', $new_url, $post_id ); - - return $new_url; - } - - /** - * Filters the list of attachment image attributes. - * - * @param array $attr Attributes for the image markup. - * @param WP_Post $attachment Image attachment post. - * @param string|array $size Requested size. Image size or array of width and height values (in that order). - * - * @return array - */ - public function wp_get_attachment_image_attributes( $attr, $attachment, $size ) { - if ( ! ( $as3cf_item = $this->is_attachment_served_by_provider( $attachment->ID ) ) ) { - return $attr; - } - - $size = AS3CF_Utils::maybe_convert_size_to_string( $attachment->ID, $size ); - - // image_downsize incorrectly substitutes size filename into full URL for src attribute instead of clobbering. - // So we need to fix up the src attribute if a size is being used. - if ( ! empty( $size ) && ! empty( $attr['src'] ) ) { - $attr['src'] = $this->get_attachment_provider_url( $attachment->ID, $as3cf_item, null, $size ); - } - - /** - * Filtered list of attachment image attributes. - * - * @param array $attr Attributes for the image markup. - * @param WP_Post $attachment Image attachment post. - * @param string $size Requested size. - * @param Media_Library_Item $as3cf_item - */ - return apply_filters( 'as3cf_wp_get_attachment_image_attributes', $attr, $attachment, $size, $as3cf_item ); - } - - /** - * Maybe encode attachment URLs when retrieving the image tag - * - * @param string $html - * @param int $id - * @param string $alt - * @param string $title - * @param string $align - * @param string $size - * - * @return string - */ - public function maybe_encode_get_image_tag( $html, $id, $alt, $title, $align, $size ) { - if ( ! ( $as3cf_item = $this->is_attachment_served_by_provider( $id ) ) ) { - // Not served by provider, return - return $html; - } - - if ( ! is_string( $html ) ) { - return $html; - } - - preg_match( '@\ssrc=[\'\"]([^\'\"]*)[\'\"]@', $html, $matches ); - - if ( ! isset( $matches[1] ) ) { - // Can't establish img src - return $html; - } - - $img_src = $matches[1]; - $new_img_src = $this->maybe_sign_intermediate_size( $img_src, $id, $size, $as3cf_item ); - $new_img_src = AS3CF_Utils::encode_filename_in_path( $new_img_src ); - - return str_replace( $img_src, $new_img_src, $html ); - } - - /** - * Maybe encode URLs for images that represent an attachment - * - * @param array|bool $image - * @param int $attachment_id - * @param string|array $size - * @param bool $icon - * - * @return array - */ - public function maybe_encode_wp_get_attachment_image_src( $image, $attachment_id, $size, $icon ) { - if ( ! ( $as3cf_item = $this->is_attachment_served_by_provider( $attachment_id ) ) ) { - // Not served by provider, return - return $image; - } - - if ( isset( $image[0] ) ) { - $url = $this->maybe_sign_intermediate_size( $image[0], $attachment_id, $size, $as3cf_item ); - $url = AS3CF_Utils::encode_filename_in_path( $url ); - - $image[0] = $url; - } - - return $image; - } - - /** - * Maybe encode URLs when outputting attachments in the media grid - * - * @param array $response - * @param int|object $attachment - * @param array $meta - * - * @return array - */ - public function maybe_encode_wp_prepare_attachment_for_js( $response, $attachment, $meta ) { - if ( ! ( $as3cf_item = $this->is_attachment_served_by_provider( $attachment->ID ) ) ) { - // Not served by provider, return - return $response; - } - - if ( isset( $response['url'] ) ) { - $response['url'] = AS3CF_Utils::encode_filename_in_path( $response['url'] ); - } - - if ( isset( $response['sizes'] ) && is_array( $response['sizes'] ) ) { - foreach ( $response['sizes'] as $size => $value ) { - $url = $this->maybe_sign_intermediate_size( $value['url'], $attachment->ID, $size, $as3cf_item, true ); - $url = AS3CF_Utils::encode_filename_in_path( $url ); - - $response['sizes'][ $size ]['url'] = $url; - } - } - - return $response; - } - - /** - * Maybe encode URLs when retrieving intermediate sizes. - * - * @param array $data - * @param int $post_id - * @param string|array $size - * - * @return array - */ - public function maybe_encode_image_get_intermediate_size( $data, $post_id, $size ) { - if ( ! ( $as3cf_item = $this->is_attachment_served_by_provider( $post_id ) ) ) { - // Not served by provider, return - return $data; - } - - if ( isset( $data['url'] ) ) { - $url = $this->maybe_sign_intermediate_size( $data['url'], $post_id, $size, $as3cf_item ); - $url = AS3CF_Utils::encode_filename_in_path( $url ); - - $data['url'] = $url; - } - - return $data; - } - - /** - * Sign intermediate size. - * - * @param string $url - * @param int $attachment_id - * @param string|array $size - * @param bool|Media_Library_Item $as3cf_item - * @param bool $force_rewrite If size not signed, make sure correct URL is being used anyway. - * - * @return string|WP_Error - */ - protected function maybe_sign_intermediate_size( $url, $attachment_id, $size, $as3cf_item = false, $force_rewrite = false ) { - if ( ! $as3cf_item ) { - $as3cf_item = Media_Library_Item::get_by_source_id( $attachment_id ); - } - - $size = AS3CF_Utils::maybe_convert_size_to_string( $attachment_id, $size ); - - if ( $force_rewrite || $as3cf_item->is_private_size( $size ) ) { - // Private file, add AWS signature if required - return $this->get_attachment_provider_url( $attachment_id, $as3cf_item, null, $size ); - } - - return $url; - } + return ''; + } /** * Is attachment served by provider. @@ -2744,91 +1467,34 @@ protected function maybe_sign_intermediate_size( $url, $attachment_id, $size, $a * * @return bool|Media_Library_Item */ - public function is_attachment_served_by_provider( $attachment_id, $skip_rewrite_check = false, $skip_current_provider_check = false, Storage_Provider $provider = null, $check_is_verified = false ) { - if ( ! $skip_rewrite_check && ! $this->get_setting( 'serve-from-s3' ) ) { - // Not serving provider URLs - return false; - } - - $as3cf_item = Media_Library_Item::get_by_source_id( $attachment_id ); - - if ( ! $as3cf_item ) { - // File not uploaded to a provider - return false; - } - - if ( ! $skip_rewrite_check && ! empty( $check_is_verified ) && ! $as3cf_item->is_verified() ) { - // Offload not verified, treat as not offloaded. - return false; - } - - if ( ! $skip_current_provider_check && empty( $provider ) ) { - $provider = $this->get_storage_provider(); - } - - if ( ! empty( $provider ) && $provider::get_provider_key_name() !== $as3cf_item->provider() ) { - // File not uploaded to required provider - return false; - } - - return $as3cf_item; - } - - /** - * Allow processes to update the file on provider via update_attached_file() - * - * @param string $file - * @param int $attachment_id - * - * @return string - */ - function update_attached_file( $file, $attachment_id ) { - if ( ! $this->is_plugin_setup( true ) ) { - return $file; + public function is_attachment_served_by_provider( $attachment_id, $skip_rewrite_check = false, $skip_current_provider_check = false, Storage_Provider $provider = null, $check_is_verified = false ) { + if ( ! $skip_rewrite_check && ! $this->get_setting( 'serve-from-s3' ) ) { + // Not serving provider URLs + return false; } $as3cf_item = Media_Library_Item::get_by_source_id( $attachment_id ); if ( ! $as3cf_item ) { - return $file; + // File not uploaded to a provider + return false; } - $file = apply_filters( 'as3cf_update_attached_file', $file, $attachment_id, $as3cf_item ); - - return $file; - } - - /** - * Return the provider URL when the local file is missing - * unless we know who the calling process is and we are happy - * to copy the file back to the server to be used. - * - * @handles get_attached_file - * @handles wp_get_original_image_path - * - * @param string $file - * @param int $attachment_id - * - * @return string - */ - function get_attached_file( $file, $attachment_id ) { - $as3cf_item = $this->is_attachment_served_by_provider( $attachment_id ); - - if ( file_exists( $file ) || ! $as3cf_item ) { - if ( $as3cf_item ) { - // Although we have a local file, give filter implementors a chance to override or copy back siblings. - return apply_filters( 'as3cf_get_attached_file_noop', $file, $file, $attachment_id, $as3cf_item ); - } else { - return $file; - } + if ( ! $skip_rewrite_check && ! empty( $check_is_verified ) && ! $as3cf_item->is_verified() ) { + // Offload not verified, treat as not offloaded. + return false; } - $url = $this->get_attachment_url( $attachment_id ); + if ( ! $skip_current_provider_check && empty( $provider ) ) { + $provider = $this->get_storage_provider(); + } - // return the URL by default - $file = apply_filters( 'as3cf_get_attached_file', $url, $file, $attachment_id, $as3cf_item ); + if ( ! empty( $provider ) && $provider::get_provider_key_name() !== $as3cf_item->provider() ) { + // File not uploaded to required provider + return false; + } - return $file; + return $as3cf_item; } /** @@ -3068,7 +1734,7 @@ public function our_screen( WP_Screen $screen = null ) { /** * Add the settings page to the top-level AWS menu item for backwards compatibility. * - * @param \Amazon_Web_Services $aws Plugin class instance from the amazon-web-services plugin. + * @param Amazon_Web_Services $aws Plugin class instance from the amazon-web-services plugin. */ public function aws_admin_menu( $aws ) { $aws->add_page( @@ -3283,7 +1949,7 @@ function check_write_permission( $bucket = null, $region = null ) { return self::$buckets_check[ $bucket ]; } - $key = $this->get_file_prefix() . 'as3cf-permission-check.txt'; + $key = $this->get_simple_file_prefix() . 'as3cf-permission-check.txt'; $file_contents = __( 'This is a test file to check if the user has write permission to the bucket. Delete me if found.', 'amazon-s3-and-cloudfront' ); $can_write = $this->get_provider_client( $region, true )->can_write( $bucket, $key, $file_contents ); @@ -3301,6 +1967,28 @@ function check_write_permission( $bucket = null, $region = null ) { return $can_write; } + /** + * Get the file prefix for test and display purposes. + * + * Note: This should only be used for "naive" prefix calculations for + * display and write permission test purposes + * + * @param null|string $time + * @param bool $object_versioning_allowed Can an Object Versioning string be appended if setting turned on? Default true. + * + * @return string + */ + private function get_simple_file_prefix( $time = null, $object_versioning_allowed = true ) { + $prefix = AS3CF_Utils::trailingslash_prefix( $this->get_object_prefix() ); + $prefix .= AS3CF_Utils::trailingslash_prefix( $this->get_dynamic_prefix( $time ) ); + + if ( ! empty( $object_versioning_allowed ) && $this->get_setting( 'object-versioning' ) ) { + $prefix .= AS3CF_Utils::trailingslash_prefix( $this->get_object_version_string() ); + } + + return $prefix; + } + /** * Render error messages in a view for bucket permission and access issues * @@ -4010,10 +2698,11 @@ function render_addons( $addons = null ) { * year month subdirectory setting and just uses S3 setting * * @param string $time + * @param bool $can_use_yearmonth * * @return string */ - function get_dynamic_prefix( $time = null ) { + public function get_dynamic_prefix( $time = null, $can_use_yearmonth = true ) { $prefix = ''; $subdir = ''; @@ -4056,7 +2745,7 @@ function get_dynamic_prefix( $time = null ) { } } - if ( $this->get_setting( 'use-yearmonth-folders' ) ) { + if ( $this->get_setting( 'use-yearmonth-folders' ) && $can_use_yearmonth ) { $subdir = $this->get_year_month_directory_name( $time ); $prefix .= $subdir; } @@ -4153,9 +2842,9 @@ function is_pro() { * @param Media_Library_Item $as3cf_item * @param string|null $size */ - function make_acl_admin_notice( Media_Library_Item $as3cf_item, $size = null ) { + public function make_acl_admin_notice( Media_Library_Item $as3cf_item, $size = null ) { $filename = wp_basename( $as3cf_item->path( $size ) ); - $acl = $as3cf_item->is_private_size( $size ) ? $this->get_storage_provider()->get_private_acl() : $this->get_storage_provider()->get_default_acl(); + $acl = $as3cf_item->is_private( $size ) ? $this->get_storage_provider()->get_private_acl() : $this->get_storage_provider()->get_default_acl(); $acl_name = $this->get_acl_display_name( $acl ); $text = sprintf( __( 'WP Offload Media — The file %s has been given %s permissions in the bucket.', 'amazon-s3-and-cloudfront' ), "{$filename}", "{$acl_name}" ); @@ -5043,7 +3732,7 @@ function get_access_denied_notice_message( $single = true ) { * Used to give a realistic total of storage space used on a Multisite subsite, * when there have been attachments uploaded to S3 but removed from server * - * @param $space_used bool + * @param bool $space_used * * @return float|int */ @@ -5133,10 +3822,12 @@ public function media_counts( $skip_transient = false, $force = false ) { foreach ( $table_prefixes as $blog_id => $table_prefix ) { $this->switch_to_blog( $blog_id ); - $counts = Media_Library_Item::count_attachments( $skip_transient, $force ); - $total += $counts['total']; - $offloaded += $counts['offloaded']; - $not_offloaded += $counts['not_offloaded']; + foreach ( $this->get_source_type_classes() as $class ) { + $counts = $class::count_items( $skip_transient, $force ); + $total += $counts['total']; + $offloaded += $counts['offloaded']; + $not_offloaded += $counts['not_offloaded']; + } $this->restore_current_blog(); } @@ -5295,17 +3986,6 @@ public function filter_input( $variable, $type = INPUT_GET, $filter = FILTER_DEF return filter_input( $type, $variable, $filter, $options ); } - /** - * Helper function for terminating script execution. Easily testable. - * - * @param int|string $exit_code - * - * @return void - */ - public function _exit( $exit_code = 0 ) { - exit( $exit_code ); - } - /** * Upgrade the 'virtual host' / 'bucket as domain' setting to the * new CloudFront / Domain setting @@ -5371,210 +4051,6 @@ public function maybe_update_delivery_path( $path, $domain, $timestamp = null ) return $path; } - /** - * Add the S3 meta box to the attachment screen - */ - public function attachment_provider_meta_box() { - add_meta_box( - 's3-actions', - __( 'Offload', 'amazon-s3-and-cloudfront' ), - array( $this, 'attachment_provider_actions_meta_box' ), - 'attachment', - 'side', - 'core' - ); - } - - /** - * Check we can do the media actions - * - * @return bool - */ - public function verify_media_actions() { - return false; - } - - /** - * Get a list of available media actions which can be performed according to plugin and user capability requirements. - * - * @param string|null $scope - * - * @return array - */ - public function get_available_media_actions( $scope = '' ) { - return array(); - } - - /** - * Render the S3 attachment meta box - */ - public function attachment_provider_actions_meta_box() { - global $post; - $file = get_attached_file( $post->ID, true ); - - $args = array( - 'provider_object' => $this->get_formatted_provider_info( $post->ID ), - 'post' => $post, - 'local_file_exists' => file_exists( $file ), - 'available_actions' => $this->get_available_media_actions( 'singular' ), - 'sendback' => 'post.php?post=' . $post->ID . '&action=edit', - ); - - $this->render_view( 'attachment-metabox', $args ); - } - - /** - * Get ACL value string. - * - * @param array $acl - * @param int $post_id - * - * @return string - */ - protected function get_acl_value_string( $acl, $post_id ) { - return $acl['name']; - } - - /** - * Return a formatted provider info array with display friendly defaults - * - * @param int $id - * - * @return bool|array - */ - public function get_formatted_provider_info( $id ) { - $as3cf_item = Media_Library_Item::get_by_source_id( $id ); - - if ( ! $as3cf_item ) { - return false; - } - - $provider_object = $as3cf_item->key_values(); - - // Backwards compatibility. - $provider_object['key'] = $provider_object['path']; - $provider_object['url'] = $this->get_attachment_provider_url( $id, $as3cf_item ); - - $acl = $as3cf_item->is_private() ? $this->get_storage_provider()->get_private_acl() : $this->get_storage_provider()->get_default_acl(); - $acl_info = array( - 'acl' => $acl, - 'name' => $this->get_acl_display_name( $acl ), - 'title' => $this->get_media_action_strings( 'change_to_private' ), - ); - - if ( $as3cf_item->is_private() ) { - $acl_info['title'] = $this->get_media_action_strings( 'change_to_public' ); - } - - $provider_object['acl'] = $acl_info; - $provider_object['region'] = $this->get_storage_provider()->get_region_name( $provider_object['region'] ); - $provider_object['provider_name'] = $this->get_provider_service_name( $provider_object['provider'] ); - - return $provider_object; - } - - /** - * Get all strings or a specific string used for the media actions - * - * @param null|string $string - * - * @return array|string - */ - public function get_media_action_strings( $string = null ) { - $not_verified_value = __( 'No', 'amazon-s3-and-cloudfront' ); - $not_verified_value .= ' '; - $not_verified_value .= $this->more_info_link( '/wp-offload-media/doc/add-metadata-tool/', 'os3+attachment+metabox', 'analyze-and-repair', 'More Info', '(', ')' ); - - $strings = apply_filters( 'as3cf_media_action_strings', array( - 'provider' => _x( 'Storage Provider', 'Storage provider key name', 'amazon-s3-and-cloudfront' ), - 'provider_name' => _x( 'Storage Provider', 'Storage provider name', 'amazon-s3-and-cloudfront' ), - 'bucket' => _x( 'Bucket', 'Bucket name', 'amazon-s3-and-cloudfront' ), - 'key' => _x( 'Path', 'Path to file in bucket', 'amazon-s3-and-cloudfront' ), - 'region' => _x( 'Region', 'Location of bucket', 'amazon-s3-and-cloudfront' ), - 'acl' => _x( 'Access', 'Access control list of the file in bucket', 'amazon-s3-and-cloudfront' ), - 'url' => __( 'URL', 'amazon-s3-and-cloudfront' ), - 'is_verified' => _x( 'Verified', 'Whether or not metadata has been verified', 'amazon-s3-and-cloudfront' ), - 'not_verified' => $not_verified_value, - ) ); - - if ( ! is_null( $string ) ) { - return isset( $strings[ $string ] ) ? $strings[ $string ] : ''; - } - - return $strings; - } - - /** - * Load media assets. - */ - public function load_media_assets() { - $this->enqueue_style( 'as3cf-media-styles', 'assets/css/media', array( 'as3cf-modal' ) ); - $this->enqueue_script( 'as3cf-media-script', 'assets/js/media', array( - 'jquery', - 'media-views', - 'media-grid', - 'wp-util', - ) ); - - wp_localize_script( 'as3cf-media-script', 'as3cf_media', array( - 'strings' => $this->get_media_action_strings(), - 'nonces' => array( - 'get_attachment_provider_details' => wp_create_nonce( 'get-attachment-s3-details' ), - ), - ) ); - } - - /** - * Handle retrieving the provider details for attachment modals. - */ - public function ajax_get_attachment_provider_details() { - if ( ! isset( $_POST['id'] ) ) { - return; - } - - check_ajax_referer( 'get-attachment-s3-details', '_nonce' ); - - $id = intval( $_POST['id'] ); - - // get the actions available for the attachment - $data = array( - 'links' => $this->add_media_row_actions( array(), $id ), - 'provider_object' => $this->get_formatted_provider_info( $id ), - 'acl_toggle' => $this->verify_media_actions() && $this->is_attachment_served_by_provider( $id, true ), - ); - - wp_send_json_success( $data ); - } - - /** - * Conditionally adds copy, remove and download S3 action links for an - * attachment on the Media library list view - * - * @param array $actions - * @param WP_Post|int $post - * - * @return array - */ - function add_media_row_actions( array $actions, $post ) { - return $actions; - } - - /** - * Load the attachment assets only when editing an attachment - * - * @param $hook_suffix - */ - public function load_attachment_assets( $hook_suffix ) { - global $post; - if ( 'post.php' !== $hook_suffix || 'attachment' !== $post->post_type ) { - return; - } - - $this->enqueue_style( 'as3cf-pro-attachment-styles', 'assets/css/attachment', array( 'as3cf-modal' ) ); - - do_action( 'as3cf_load_attachment_assets' ); - } - /** * Maybe remove query string from URL. * @@ -5588,40 +4064,6 @@ public function maybe_remove_query_string( $url ) { return reset( $parts ); } - /** - * Has the given attachment been uploaded by this instance? - * - * @param int $attachment_id - * - * @return bool - */ - public function attachment_just_uploaded( $attachment_id ) { - if ( is_int( $attachment_id ) && in_array( $attachment_id, $this->uploaded_post_ids ) ) { - return true; - } - - return false; - } - - /** - * Filters the audio & video shortcodes output to remove "&_=NN" params from source.src as it breaks signed URLs. - * - * @param string $html Shortcode HTML output. - * @param array $atts Array of shortcode attributes. - * @param string $media Media file. - * @param int $post_id Post ID. - * @param string $library Media library used for the shortcode. - * - * @return string - * - * Note: Depends on 30377.4.diff from https://core.trac.wordpress.org/ticket/30377 - */ - public function wp_media_shortcode( $html, $atts, $media, $post_id, $library ) { - $html = preg_replace( '/&_=[0-9]+/', '', $html ); - - return $html; - } - /** * Ensure local URL is correct for multisite's non-primary subsites. * @@ -5662,7 +4104,7 @@ public function get_acl_for_intermediate_size( $attachment_id, $size, $bucket = $acl = $this->get_storage_provider()->get_default_acl(); if ( ! empty( $as3cf_item ) ) { - $acl = $as3cf_item->is_private_size( $size ) ? $this->get_storage_provider()->get_private_acl() : $this->get_storage_provider()->get_default_acl(); + $acl = $as3cf_item->is_private( $size ) ? $this->get_storage_provider()->get_private_acl() : $this->get_storage_provider()->get_default_acl(); } } @@ -5672,14 +4114,15 @@ public function get_acl_for_intermediate_size( $attachment_id, $size, $bucket = /** * Are ACLs in use for intermediate size on bucket? * - * @param int $attachment_id - * @param string $size - * @param string $bucket Optional bucket that ACL is potentially to be used with. - * @param Media_Library_Item|null $as3cf_item Optional item. + * @param int $attachment_id + * @param string $size + * @param string $bucket Optional bucket that ACL is potentially to be used with. + * @param Item|null $as3cf_item Optional item. * * @return bool */ - public function use_acl_for_intermediate_size( $attachment_id, $size, $bucket = null, Media_Library_Item $as3cf_item = null ) { + public function use_acl_for_intermediate_size( $attachment_id, $size, $bucket = null, Item $as3cf_item = null ) { + // If this function is used without passing in an Item object, we're assuming $attachment if ( empty( $as3cf_item ) ) { $as3cf_item = Media_Library_Item::get_by_source_id( $attachment_id ); } @@ -5696,7 +4139,7 @@ public function use_acl_for_intermediate_size( $attachment_id, $size, $bucket = $use_private_prefix = apply_filters( 'as3cf_enable_signed_urls_for_intermediate_size', $this->private_prefix_enabled(), $attachment_id, $size, $bucket, $as3cf_item ); // If signed custom URLs are in play, and we have a private object, usually you can not use ACLs. - if ( $use_acl && $use_private_prefix && ! empty( $as3cf_item ) && $as3cf_item->is_private_size( $size ) ) { + if ( $use_acl && $use_private_prefix && ! empty( $as3cf_item ) && $as3cf_item->is_private( $size ) ) { $use_acl = false; } @@ -5704,25 +4147,6 @@ public function use_acl_for_intermediate_size( $attachment_id, $size, $bucket = return apply_filters( 'as3cf_use_acl_for_intermediate_size', $use_acl, $attachment_id, $size, $bucket, $as3cf_item ); } - /** - * Consolidate an array of WP_Errors into a single WP_Error object. - * - * @param array $upload_errors - * - * @return WP_Error - */ - protected function consolidate_upload_errors( $upload_errors ) { - $errors = new WP_Error; - - foreach ( $upload_errors as $error ) { - - /* @var WP_Error $error */ - $errors->add( $error->get_error_code(), $error->get_error_message() ); - } - - return $errors; - } - /** * Get all defined addons that use this plugin * @@ -5838,46 +4262,6 @@ public function handle_aws_access_key_form_header() { $this->render_view( 'notice', $notice ); } - /** - * Remove 'filesize' from attachment's metatdata if appropriate, also our total filesize record. - * - * @param integer $post_id Attachment's post_id. - * @param array $data Attachment's metadata. - * @param bool $update_metadata Update the metadata record now? Defaults to true. - * - * @return array Attachment's cleaned up metadata. - */ - public function maybe_cleanup_filesize_metadata( $post_id, $data, $update_metadata = true ) { - if ( ! is_int( $post_id ) || empty( $post_id ) || empty( $data ) || ! is_array( $data ) ) { - return $data; - } - - /* - * Audio and video have a filesize added to metadata by default, but images and anything else don't. - * Note: Could have used `wp_generate_attachment_metadata` here to test whether default metadata has 'filesize', - * but it not only has side effects it also does a lot of work considering it's not a huge deal for this entry to hang around. - */ - if ( - empty( $data['mime_type'] ) || - 0 === strpos( $data['mime_type'], 'image/' ) || - ! ( 0 === strpos( $data['mime_type'], 'audio/' ) || 0 === strpos( $data['mime_type'], 'video/' ) ) - ) { - unset( $data['filesize'] ); - } - - if ( $update_metadata ) { - if ( empty( $data ) ) { - delete_post_meta( $post_id, '_wp_attachment_metadata' ); - } else { - update_post_meta( $post_id, '_wp_attachment_metadata', $data ); - } - } - - delete_post_meta( $post_id, 'as3cf_filesize_total' ); - - return $data; - } - /** * Is there an upgrade in progress? * @@ -5903,4 +4287,87 @@ public function private_prefix_enabled() { return false; } + + /** + * Register an item type name and class + * + * @param string $source_type + * @param string $class + */ + public function register_source_type( $source_type, $class ) { + $this->source_type_classes[ $source_type ] = $class; + } + + /** + * Get Item type class from item type identifier + * + * @param string $source_type + * + * @return string|false + */ + public function get_source_type_class( $source_type ) { + if ( isset( $this->source_type_classes[ $source_type ] ) ) { + return $this->source_type_classes[ $source_type ]; + } + + return false; + } + + /** + * Get Item type human friendly name item type identifier + * + * @param string $source_type + * + * @return string|false + */ + public function get_source_type_name( $source_type ) { + /** @var Item $class */ + $class = $this->get_source_type_class( $source_type ); + if ( ! empty( $class ) ) { + return $class::source_type_name(); + } + + return false; + } + + /** + * Get all registered Item classes + * + * @return array + */ + public function get_source_type_classes() { + return $this->source_type_classes; + } + + /** + * Returns the Item_Handler instance for the given handler type. + * + * @param string $handler_type + * + * @return Item_Handler + */ + public function get_item_handler( $handler_type ) { + if ( isset( $this->item_handlers[ $handler_type ] ) ) { + return $this->item_handlers[ $handler_type ]; + } + + switch ( $handler_type ) { + case Upload_Handler::get_item_handler_key_name(): + $this->item_handlers[ $handler_type ] = new Upload_Handler( $this ); + break; + case Download_Handler::get_item_handler_key_name(): + $this->item_handlers[ $handler_type ] = new Download_Handler( $this ); + break; + case Remove_Local_Handler::get_item_handler_key_name(): + $this->item_handlers[ $handler_type ] = new Remove_Local_Handler( $this ); + break; + case Remove_Provider_Handler::get_item_handler_key_name(): + $this->item_handlers[ $handler_type ] = new Remove_Provider_Handler( $this ); + break; + default: + return null; + } + + return $this->item_handlers[ $handler_type ]; + } } diff --git a/classes/as3cf-filter.php b/classes/as3cf-filter.php index 31630cf3..6edad1f4 100644 --- a/classes/as3cf-filter.php +++ b/classes/as3cf-filter.php @@ -1,5 +1,8 @@ $attachment ) { - $url = $this->get_url( $attachment['attachment_id'] ); + $item_source = array( + 'id' => $attachment['attachment_id'], + 'source_type' => Media_Library_Item::source_type(), + ); + $url = $this->get_url( $item_source ); if ( $url ) { $value[ $key ]['file'] = $url; @@ -113,7 +119,11 @@ public function filter_customizer_image( $value, $old_value = false ) { * @return stdClass */ public function filter_header_image_data( $value, $old_value = false ) { - $url = $this->get_url( $value->attachment_id ); + $item_source = array( + 'id' => $value->attachment_id, + 'source_type' => Media_Library_Item::source_type(), + ); + $url = $this->get_url( $item_source ); if ( $url ) { $value->url = $url; @@ -148,16 +158,17 @@ public function filter_post( $content ) { /** * Handle widget instances. * - * @param array $instance - * @param WP_Widget $class + * @param array $instance * * @return array */ - protected function handle_widget( $instance, $class ) { - if ( empty( $instance ) ) { + protected function handle_widget( $instance ) { + if ( empty( $instance ) || ! is_array( $instance ) ) { return $instance; } + $cache = $this->get_option_cache(); + $to_cache = array(); $update_cache = true; // Editing widgets in Customizer throws an error if more than one option record is updated. @@ -166,39 +177,12 @@ protected function handle_widget( $instance, $class ) { $update_cache = false; } - if ( $class instanceof WP_Widget_Media ) { - return $this->filter_media_widget( $instance, $update_cache ); - } - - if ( $class instanceof WP_Widget_Text ) { - return $this->filter_text_widget( $instance, $update_cache ); - } - - if ( $class instanceof WP_Widget_Custom_HTML ) { - return $this->filter_custom_html_widget( $instance, $update_cache ); - } - - return $instance; - } - - /** - * Filter media widget. - * - * @param array $instance - * @param bool $update_cache - * - * @return array - */ - protected function filter_media_widget( $instance, $update_cache ) { - $cache = $this->get_option_cache(); - $to_cache = array(); - foreach ( $instance as $key => $value ) { if ( empty( $value ) ) { continue; } - if ( AS3CF_Utils::is_url( $value ) ) { + if ( in_array( $key, array( 'text', 'content' ) ) || AS3CF_Utils::is_url( $value ) ) { $instance[ $key ] = $this->process_content( $value, $cache, $to_cache ); } } @@ -210,46 +194,6 @@ protected function filter_media_widget( $instance, $update_cache ) { return $instance; } - /** - * Filter text widget. - * - * @param array $instance - * @param bool $update_cache - * - * @return array - */ - protected function filter_text_widget( $instance, $update_cache ) { - $cache = $this->get_option_cache(); - $to_cache = array(); - $instance['text'] = $this->process_content( $instance['text'], $cache, $to_cache ); - - if ( $update_cache ) { - $this->maybe_update_option_cache( $to_cache ); - } - - return $instance; - } - - /** - * Filter custom html widget. - * - * @param array $instance - * @param bool $update_cache - * - * @return array - */ - protected function filter_custom_html_widget( $instance, $update_cache ) { - $cache = $this->get_option_cache(); - $to_cache = array(); - $instance['content'] = $this->process_content( $instance['content'], $cache, $to_cache ); - - if ( $update_cache ) { - $this->maybe_update_option_cache( $to_cache ); - } - - return $instance; - } - /** * Process content. * @@ -326,8 +270,8 @@ protected function get_urls_from_img_src( $content, &$to_cache ) { return $url_pairs; } - $matches = array_unique( $matches[0] ); - $attachment_ids = array(); + $matches = array_unique( $matches[0] ); + $item_sources = array(); foreach ( $matches as $image ) { if ( ! preg_match( '/wp-image-([0-9]+)/i', $image, $class_id ) || ! isset( $class_id[1] ) ) { @@ -349,26 +293,29 @@ protected function get_urls_from_img_src( $content, &$to_cache ) { $url = AS3CF_Utils::reduce_url( $url ); - $attachment_ids[ $url ] = absint( $class_id[1] ); + $item_sources[ $url ] = array( + 'id' => absint( $class_id[1] ), + 'source_type' => Media_Library_Item::source_type(), + ); } - if ( count( $attachment_ids ) > 1 ) { + if ( count( $item_sources ) > 1 ) { /* * Warm object cache for use with 'get_post_meta()'. * * To avoid making a database call for each image, a single query * warms the object cache with the meta information for all images. */ - update_meta_cache( 'post', array_unique( array_values( $attachment_ids ) ) ); + update_meta_cache( 'post', array_unique( array_column( $item_sources, 'id' ) ) ); } - foreach ( $attachment_ids as $url => $attachment_id ) { - if ( ! $this->attachment_id_matches_src( $attachment_id, $url ) ) { + foreach ( $item_sources as $url => $item_source ) { + if ( ! $this->item_matches_src( $item_source, $url ) ) { // Path doesn't match attachment, skip continue; } - $this->push_to_url_pairs( $url_pairs, $attachment_id, $url, $to_cache ); + $this->push_to_url_pairs( $url_pairs, $item_source, $url, $to_cache ); } return $url_pairs; @@ -418,46 +365,46 @@ protected function get_urls_from_content( $content, $cache, &$to_cache ) { continue; } - $attachment_id = null; - $bare_url = AS3CF_Utils::reduce_url( $url ); + $item_source = null; + $bare_url = AS3CF_Utils::reduce_url( $url ); // If attachment ID recently or previously cached, skip full search. if ( isset( $to_cache[ $bare_url ] ) ) { - $attachment_id = $to_cache[ $bare_url ]; + $item_source = $to_cache[ $bare_url ]; - if ( $this->is_failure( $attachment_id ) ) { + if ( $this->is_failure( $item_source ) ) { // Attachment ID failure, continue continue; } } elseif ( isset( $cache[ $bare_url ] ) ) { - $attachment_id = $cache[ $bare_url ]; + $item_source = $cache[ $bare_url ]; - if ( $this->is_failure( $attachment_id ) ) { + if ( $this->is_failure( $item_source ) ) { // Attachment ID failure, continue continue; } } - if ( is_null( $attachment_id ) || is_array( $attachment_id ) ) { + if ( is_null( $item_source ) || ( is_array( $item_source ) && ! empty( $item_source['timestamp'] ) ) ) { // Attachment ID not cached, need to search by URL. $urls[] = $bare_url; } else { - $this->push_to_url_pairs( $url_pairs, $attachment_id, $bare_url, $to_cache ); + $this->push_to_url_pairs( $url_pairs, $item_source, $bare_url, $to_cache ); } } if ( ! empty( $urls ) ) { - $attachments = $this->get_attachment_ids_from_urls( $urls ); + $item_sources = $this->get_item_sources_from_urls( $urls ); - foreach ( $attachments as $url => $attachment_id ) { - if ( ! $attachment_id ) { - // Can't determine attachment ID, continue + foreach ( $item_sources as $url => $item_source ) { + if ( ! $item_source ) { + // Can't determine item ID, continue $this->url_cache_failure( $url, $to_cache ); continue; } - $this->push_to_url_pairs( $url_pairs, $attachment_id, $url, $to_cache ); + $this->push_to_url_pairs( $url_pairs, $item_source, $url, $to_cache ); } } @@ -467,7 +414,7 @@ protected function get_urls_from_content( $content, $cache, &$to_cache ) { /** * Is failure? * - * @param mixed $value + * @param array $value * * @return bool */ @@ -488,20 +435,23 @@ protected function is_failure( $value ) { /** * Does attachment ID match src? * - * @param int $attachment_id + * @param array $item_source * @param string $url * * @return bool */ - protected function attachment_id_matches_src( $attachment_id, $url ) { - $meta = get_post_meta( $attachment_id, '_wp_attachment_metadata', true ); + public function item_matches_src( $item_source, $url ) { + if ( empty( $item_source['id'] ) || empty( $item_source['source_type'] ) || Media_Library_Item::source_type() !== $item_source['source_type'] ) { + return false; + } + $meta = get_post_meta( $item_source['id'], '_wp_attachment_metadata', true ); if ( ! isset( $meta['sizes'] ) ) { // No sizes found, return return false; } - $base_url = AS3CF_Utils::encode_filename_in_path( AS3CF_Utils::reduce_url( $this->get_base_url( $attachment_id ) ) ); + $base_url = AS3CF_Utils::encode_filename_in_path( AS3CF_Utils::reduce_url( $this->get_base_url( $item_source ) ) ); $basename = wp_basename( $base_url ); // Add full size URL @@ -526,31 +476,31 @@ protected function attachment_id_matches_src( $attachment_id, $url ) { * Push to URL pairs. * * @param array $url_pairs - * @param int $attachment_id + * @param array $item_source * @param string $find * @param array $to_cache */ - protected function push_to_url_pairs( &$url_pairs, $attachment_id, $find, &$to_cache ) { + protected function push_to_url_pairs( &$url_pairs, $item_source, $find, &$to_cache ) { $find_full = AS3CF_Utils::remove_size_from_filename( $find ); $find_full = $this->normalize_find_value( $this->as3cf->maybe_remove_query_string( $find_full ) ); $find_size = $this->normalize_find_value( $this->as3cf->maybe_remove_query_string( $find ) ); // Cache find URLs even if no replacement. - $to_cache[ $find_full ] = $attachment_id; + $to_cache[ $find_full ] = $item_source; if ( wp_basename( $find_full ) !== wp_basename( $find_size ) ) { - $to_cache[ $find_size ] = $attachment_id; + $to_cache[ $find_size ] = $item_source; } - $replace_full = $this->get_url( $attachment_id ); + $replace_full = $this->get_url( $item_source ); // Replacement URL can't be found. if ( ! $replace_full ) { return; } - $size = $this->get_size_string_from_url( $attachment_id, $find ); - $replace_size = $this->get_url( $attachment_id, $size ); + $size = $this->get_size_string_from_url( $item_source, $find ); + $replace_size = $this->get_url( $item_source, $size ); $parts = parse_url( $find ); if ( ! isset( $parts['scheme'] ) ) { @@ -570,35 +520,24 @@ protected function push_to_url_pairs( &$url_pairs, $attachment_id, $find, &$to_c $replace_full = $this->as3cf->maybe_remove_query_string( $replace_full ); $replace_size = $this->as3cf->maybe_remove_query_string( $replace_size ); - $to_cache[ $this->normalize_find_value( $replace_full ) ] = $attachment_id; - $to_cache[ $this->normalize_find_value( $replace_size ) ] = $attachment_id; + $to_cache[ $this->normalize_find_value( $replace_full ) ] = $item_source; + $to_cache[ $this->normalize_find_value( $replace_size ) ] = $item_source; } /** * Get size string from URL. * - * @param int $attachment_id + * @param array $item_source * @param string $url * * @return null|string */ - public function get_size_string_from_url( $attachment_id, $url ) { - $meta = get_post_meta( $attachment_id, '_wp_attachment_metadata', true ); - - if ( empty( $meta['sizes'] ) ) { - // No alternative sizes available, return - return null; - } - - $basename = AS3CF_Utils::encode_filename_in_path( wp_basename( $this->as3cf->maybe_remove_query_string( $url ) ) ); - - foreach ( $meta['sizes'] as $size => $file ) { - if ( $basename === AS3CF_Utils::encode_filename_in_path( $file['file'] ) ) { - return $size; - } + public function get_size_string_from_url( $item_source, $url ) { + if ( empty( $item_source['id'] ) || empty( $item_source['source_type'] ) ) { + return false; } - return null; + return apply_filters( 'as3cf_get_size_string_from_url_for_item_source', Item::primary_object_key(), $url, $item_source ); } /** @@ -659,11 +598,14 @@ protected function url_replaced( $find, $replace, $content ) { /** * Get post cache * - * @param null|int|WP_Post $post Optional. Post ID or post object. Defaults to current post. + * @param null|int|WP_Post $post Optional. Post ID or post object. Defaults to current post. + * @param bool $transform_ints Optional. If true (default), convert integer hits to array with id and source_type keys. + * If false, return integer hits as integers * - * @return array + * + * @return array|int */ - public function get_post_cache( $post = null ) { + public function get_post_cache( $post = null, $transform_ints = true ) { $post_id = AS3CF_Utils::get_post_id( $post ); if ( ! $post_id ) { @@ -680,6 +622,21 @@ public function get_post_cache( $post = null ) { $cache = array(); } + if ( ! $transform_ints ) { + return $cache; + } + + // Handle old cache items that are stored as plain integers + foreach ( $cache as &$cache_item ) { + if ( ! is_array( $cache_item ) && is_numeric( $cache_item ) ) { + $id = $cache_item; + $cache_item = array( + 'id' => $id, + 'source_type' => Media_Library_Item::source_type(), + ); + } + } + return $cache; } @@ -731,7 +688,7 @@ protected function maybe_update_post_cache( $to_cache, $post_id = false ) { return; } - $cached = $this->get_post_cache( $post_id ); + $cached = $this->get_post_cache( $post_id, false ); $urls = static::merge_cache( $cached, $to_cache ); if ( $urls !== $cached ) { @@ -777,13 +734,17 @@ protected function maybe_update_option_cache( $to_cache ) { } /** - * Purge attachment from cache on delete. + * Purge items from cache on delete. * * @param int $post_id */ public function purge_cache_on_attachment_delete( $post_id ) { if ( ! in_array( $post_id, self::$purged_ids ) ) { - $this->purge_from_cache( $this->get_url( $post_id ) ); + $item_source = array( + 'id' => $post_id, + 'source_type' => Media_Library_Item::source_type(), + ); + $this->purge_from_cache( $this->get_url( $item_source ) ); self::$purged_ids[] = $post_id; } } @@ -1044,30 +1005,30 @@ abstract protected function url_needs_replacing( $url ); /** * Get URL. * - * @param int $attachment_id - * @param null|string $size + * @param int|array $item_source + * @param null|string $object_key * * @return bool|string */ - abstract protected function get_url( $attachment_id, $size = null ); + abstract protected function get_url( $item_source, $object_key = null ); /** * Get base URL. * - * @param int $attachment_id + * @param int|array $item_source * * @return string|false */ - abstract protected function get_base_url( $attachment_id ); + abstract protected function get_base_url( $item_source ); /** * Get attachment ID from URL. * * @param string $url * - * @return bool|int + * @return array */ - abstract protected function get_attachment_id_from_url( $url ); + abstract public function get_item_source_from_url( $url ); /** * Get attachment IDs from URLs. @@ -1076,7 +1037,7 @@ abstract protected function get_attachment_id_from_url( $url ); * * @return array url => attachment ID (or false) */ - abstract protected function get_attachment_ids_from_urls( $urls ); + abstract protected function get_item_sources_from_urls( $urls ); /** * Normalize find value. diff --git a/classes/as3cf-plugin-compatibility.php b/classes/as3cf-plugin-compatibility.php index 16454811..026258f3 100644 --- a/classes/as3cf-plugin-compatibility.php +++ b/classes/as3cf-plugin-compatibility.php @@ -9,7 +9,11 @@ * @since 0.8.3 */ +use DeliciousBrains\WP_Offload_Media\Integrations\Media_Library; +use DeliciousBrains\WP_Offload_Media\Items\Download_Handler; +use DeliciousBrains\WP_Offload_Media\Items\Item; use DeliciousBrains\WP_Offload_Media\Items\Media_Library_Item; +use DeliciousBrains\WP_Offload_Media\Items\Remove_Provider_Handler; use DeliciousBrains\WP_Offload_Media\Providers\Storage\Storage_Provider; // Exit if accessed directly @@ -26,35 +30,25 @@ */ class AS3CF_Plugin_Compatibility { - /** - * @var Amazon_S3_And_CloudFront - */ - protected $as3cf; - /** * @var array */ protected static $stream_wrappers = array(); /** - * @var array - */ - protected $compatibility_addons; - - /** - * @var array + * @var Amazon_S3_And_CloudFront */ - private $removed_files = array(); + protected $as3cf; /** * @var bool */ - protected $generate_attachment_metadata_done = false; + protected $wait_for_generate_attachment_metadata = false; /** - * @var bool + * @var array */ - protected $wait_for_generate_attachment_metadata = false; + private $removed_files = array(); /** * @param Amazon_S3_And_CloudFront $as3cf @@ -110,12 +104,12 @@ function compatibility_init_if_setup() { * WP_Image_Editor * /wp-includes/class-wp-image-editor.php */ - add_action( 'as3cf_pre_upload_attachment', array( $this, 'image_editor_remove_files' ), 10, 3 ); + add_filter( 'as3cf_pre_update_attachment_metadata', array( $this, 'image_editor_remove_files' ), 10, 4 ); add_filter( 'as3cf_get_attached_file_noop', array( $this, 'image_editor_download_file' ), 10, 4 ); add_filter( 'as3cf_get_attached_file', array( $this, 'image_editor_download_file' ), 10, 4 ); - add_filter( 'as3cf_upload_attachment_local_files_to_remove', array( $this, 'image_editor_remove_original_image' ), 10, 3 ); + add_filter( 'as3cf_remove_local_files', array( $this, 'image_editor_remove_original_image' ), 10, 3 ); add_filter( 'as3cf_get_attached_file', array( $this, 'customizer_crop_download_file' ), 10, 4 ); - add_filter( 'as3cf_upload_attachment_local_files_to_remove', array( $this, 'customizer_crop_remove_original_image' ), 10, 3 ); + add_filter( 'as3cf_remove_local_files', array( $this, 'customizer_crop_remove_original_image' ), 10, 3 ); add_filter( 'wp_unique_filename', array( $this, 'customizer_crop_unique_filename' ), 10, 3 ); /* @@ -134,7 +128,7 @@ function compatibility_init_if_setup() { * WP-CLI Compatibility */ if ( defined( 'WP_CLI' ) && class_exists( 'WP_CLI' ) ) { - WP_CLI::add_hook( 'before_invoke:media regenerate', array( $this, 'enable_get_attached_file_copy_back_to_local' ) ); + WP_CLI::add_hook( 'before_invoke:media regenerate', array( $this, 'enable_copy_back_and_wait_for_generate_metadata' ) ); } } @@ -159,12 +153,24 @@ function legacy_copy_back_to_local( $url, $file, $attachment_id, Media_Library_I if ( ( $file = $this->copy_provider_file_to_server( $as3cf_item, $file ) ) ) { // Return the file if successfully downloaded from S3 return $file; - }; + } // Return S3 URL as a fallback return $url; } + /** + * Enable copying back attachments from provider + * and waiting for their metadata to be regenerated + * before re-offloading. + * + * @handles WP_CLI:before_invoke:media regenerate + */ + public function enable_copy_back_and_wait_for_generate_metadata() { + $this->enable_get_attached_file_copy_back_to_local(); + $this->wait_for_generate_attachment_metadata = true; + } + /** * Enables copying missing local files back to the server when `get_attached_file` filter is called. */ @@ -238,12 +244,13 @@ function is_ajax() { * * @handles wp_generate_attachment_metadata * - * @param $metadata + * @param mixed $metadata * * @return mixed */ public function wp_generate_attachment_metadata( $metadata ) { - $this->generate_attachment_metadata_done = true; + $this->wait_for_generate_attachment_metadata = false; + return $metadata; } @@ -253,14 +260,16 @@ public function wp_generate_attachment_metadata( $metadata ) { * * @handles as3cf_wait_for_generate_attachment_metadata * + * @param bool $wait + * * @return bool */ - public function wait_for_generate_attachment_metadata() { - if ( ! $this->wait_for_generate_attachment_metadata ) { - return false; + public function wait_for_generate_attachment_metadata( $wait ) { + if ( $this->wait_for_generate_attachment_metadata ) { + return true; } - return ! $this->generate_attachment_metadata_done; + return $wait; } /** @@ -323,23 +332,30 @@ function copy_image_to_server_on_action( $action_key, $ajax, $url, $file, Media_ if ( ( $file = $this->copy_provider_file_to_server( $as3cf_item, $file ) ) ) { // Return the file if successfully downloaded from S3 return $file; - }; + } return $url; } /** - * Get the file path of the original image file before an update + * Get the file path of the primary image file if it exists. + * + * This helper function looks at the current metadata for the Media Library item. + * In various scenarios this is useful when an item's offloaded objects + * and the attachment's metadata are not yet in sync. * - * @param int $post_id - * @param string $file_path + * @param Item $as3cf_item * * @return bool|string */ - function get_original_image_file( $post_id, $file_path ) { - // remove original main image after edit - $meta = get_post_meta( $post_id, '_wp_attachment_metadata', true ); - $original_file = trailingslashit( dirname( $file_path ) ) . wp_basename( $meta['file'] ); + private function get_original_image_file( Item $as3cf_item ) { + if ( Media_Library_Item::source_type() !== $as3cf_item->source_type() ) { + return false; + } + + $meta = get_post_meta( $as3cf_item->source_id(), '_wp_attachment_metadata', true ); + $original_file = trailingslashit( dirname( $as3cf_item->full_source_path() ) ) . wp_basename( $meta['file'] ); + if ( file_exists( $original_file ) ) { return $original_file; } @@ -351,66 +367,43 @@ function get_original_image_file( $post_id, $file_path ) { * Allow the WordPress Image Editor to remove edited version of images * if the original image is being restored and 'IMAGE_EDIT_OVERWRITE' is set * - * @param bool $pre - * @param int $post_id - * @param array $data + * @param bool $cancel True if the upload should be cancelled + * @param array $data Array describing the object being uploaded + * @param int $post_id Attachment's ID + * @param Media_Library_Item $as3cf_item The Media Library Item object if previously offloaded * * @return bool */ - public function image_editor_remove_files( $pre, $post_id, $data ) { + public function image_editor_remove_files( $cancel, $data, $post_id, $as3cf_item ) { if ( ! isset( $_POST['do'] ) || 'restore' !== $_POST['do'] ) { - return $pre; + return $cancel; } if ( ! defined( 'IMAGE_EDIT_OVERWRITE' ) || ! IMAGE_EDIT_OVERWRITE ) { - return $pre; + return $cancel; } - $as3cf_item = Media_Library_Item::get_by_source_id( $post_id ); - - if ( ! $as3cf_item ) { - return $pre; + if ( empty( $as3cf_item ) ) { + return $cancel; } - $this->remove_edited_image_files( $post_id, $as3cf_item ); - - // Update object key with original filename - $restored_filename = wp_basename( $data['file'] ); - $old_filename = wp_basename( $as3cf_item->path() ); - $item_path = str_replace( $old_filename, $restored_filename, $as3cf_item->path() ); - - $as3cf_item = new Media_Library_Item( - $as3cf_item->provider(), - $as3cf_item->region(), - $as3cf_item->bucket(), - $item_path, - $as3cf_item->is_private(), - $as3cf_item->source_id(), - $as3cf_item->source_path(), - wp_basename( $as3cf_item->original_source_path() ), - $as3cf_item->extra_info(), - $as3cf_item->id() - ); + $keys_to_remove = array(); + $pattern = '/-e[0-9]{13}(?:-[0-9]{1,4}x[0-9]{1,4})?\./'; + $objects = $as3cf_item->objects(); + foreach ( $objects as $object_key => $object ) { + if ( preg_match( $pattern, $object['source_file'] ) ) { + $keys_to_remove[] = $object_key; + unset( $objects[ $object_key ] ); + } + } + $remove_provider_handler = $this->as3cf->get_item_handler( Remove_Provider_Handler::get_item_handler_key_name() ); + $remove_provider_handler->handle( $as3cf_item, array( 'object_keys' => $keys_to_remove ) ); + // TODO: Check these following statements are required. + $as3cf_item->set_objects( $objects ); $as3cf_item->save(); - return true; - } - - /** - * Remove edited image files from S3. - * - * @param int $attachment_id - * @param Media_Library_Item $as3cf_item - */ - protected function remove_edited_image_files( $attachment_id, Media_Library_Item $as3cf_item ) { - $keys = AS3CF_Utils::get_attachment_edited_keys( $attachment_id, $as3cf_item ); - - if ( empty( $keys ) ) { - return; - } - - $this->as3cf->delete_objects( $as3cf_item->region(), $as3cf_item->bucket(), $keys ); + return $cancel; } /** @@ -434,25 +427,11 @@ function image_editor_download_file( $url, $file, $attachment_id, Media_Library_ // for the restore to be successful and edited images to be deleted from the bucket // via image_editor_remove_files() if ( isset( $_POST['do'] ) && 'restore' == $_POST['do'] ) { - $backup_sizes = get_post_meta( $attachment_id, '_wp_attachment_backup_sizes', true ); - $original_filename = $backup_sizes['full-orig']['file']; - - $as3cf_item_orig = new Media_Library_Item( - $as3cf_item->provider(), - $as3cf_item->region(), - $as3cf_item->bucket(), - $as3cf_item->normalized_path_dir() . $original_filename, - $as3cf_item->is_private(), - $as3cf_item->source_id(), - $as3cf_item->source_path(), - wp_basename( $as3cf_item->original_source_path() ), - $as3cf_item->extra_info(), - $as3cf_item->id() - ); - $orig_file = dirname( $file ) . '/' . $original_filename; - - // Copy the original file back to the server for the restore process - $this->copy_provider_file_to_server( $as3cf_item_orig, $orig_file ); + $objects = $as3cf_item->objects(); + if ( isset( $objects['full-orig'] ) ) { + // Copy the original file back to the server for the restore process + $this->copy_provider_file_to_server( $as3cf_item, $objects['full-orig']['source_file'] ); + } // Copy the edited file back to the server as well, it will be cleaned up later if ( $provider_file = $this->copy_provider_file_to_server( $as3cf_item, $file ) ) { @@ -480,27 +459,26 @@ function image_editor_download_file( $url, $file, $attachment_id, Media_Library_ /** * Allow the WordPress Image Editor to remove the main image file after it has been copied - * back from S3 after it has done the edit. + * back from the bucket after it has done the edit. * - * @param array $files - * @param int $post_id - * @param string $file_path + * @param array $files_to_remove + * @param Item $as3cf_item + * @param array $item_source * * @return array */ - function image_editor_remove_original_image( $files, $post_id, $file_path ) { + public function image_editor_remove_original_image( $files_to_remove, $as3cf_item, $item_source ) { if ( ! $this->is_ajax() ) { - return $files; + return $files_to_remove; } if ( isset( $_POST['action'] ) && 'image-editor' === sanitize_key( $_POST['action'] ) ) { // input var okay - // remove original main image after edit - if ( ( $original_file = $this->get_original_image_file( $post_id, $file_path ) ) ) { - $files[] = $original_file; + if ( ( $original_file = $this->get_original_image_file( $as3cf_item ) ) ) { + $files_to_remove[] = $original_file; } } - return $files; + return $files_to_remove; } /** @@ -538,39 +516,40 @@ public function customizer_crop_download_file( $url, $file, $attachment_id, Medi return $url; } - if ( $this->as3cf->attachment_just_uploaded( $attachment_id ) ) { + /** @var Media_Library $media_library */ + $media_library = $this->as3cf->get_integration_manager()->get_integration( 'mlib' ); + if ( $media_library->item_just_uploaded( $attachment_id ) ) { return $url; } if ( ( $file = $this->copy_provider_file_to_server( $as3cf_item, $file ) ) ) { // Return the file if successfully downloaded from bucket. return $file; - }; + } return $url; } /** * Allow the WordPress Image Editor to remove the main image file after it has been copied - * back from S3 after it has done the edit. + * back from the bucket after it has done the edit. * - * @param array $files - * @param int $post_id - * @param string $file_path + * @param array $files_to_remove + * @param Item $as3cf_item + * @param array $item_source * * @return array */ - function customizer_crop_remove_original_image( $files, $post_id, $file_path ) { + function customizer_crop_remove_original_image( $files_to_remove, $as3cf_item, $item_source ) { if ( false === $this->is_customizer_crop_action() ) { - return $files; + return $files_to_remove; } - // remove original main image after edit - if ( ( $original_file = $this->get_original_image_file( $_POST['id'], $file_path ) ) ) { - $files[] = $original_file; + if ( ( $original_file = $this->get_original_image_file( $as3cf_item ) ) ) { + $files_to_remove[] = $original_file; } - return $files; + return $files_to_remove; } /** @@ -595,7 +574,9 @@ function customizer_background_image( $post_id, $url ) { $post_id = $url; } } else { - $post_id = $this->as3cf->filter_provider->get_attachment_id_from_url( $url ); + /** @var Media_Library $media_library */ + $media_library = $this->as3cf->get_integration_manager()->get_integration( 'mlib' ); + $post_id = $media_library->get_attachment_id_from_provider_url( $url ); } // Must return null if not found. @@ -631,26 +612,11 @@ function regenerate_thumbnails_download_file( $url, $file, $attachment_id, Media * @return string|bool File if downloaded, false on failure */ public function copy_provider_file_to_server( Media_Library_Item $as3cf_item, $file ) { - $filename = wp_basename( $file ); - - // Make sure the directory exists - $dir = dirname( $file ); - if ( ! wp_mkdir_p( $dir ) ) { - $error_message = sprintf( __( 'The local directory %s does not exist and could not be created.', 'amazon-s3-and-cloudfront' ), $dir ); - AS3CF_Error::log( sprintf( __( 'There was an error attempting to download the file %s from the bucket: %s', 'amazon-s3-and-cloudfront' ), $as3cf_item->key( $filename ), $error_message ) ); - - return false; - } - - try { - $this->as3cf->get_provider_client( $as3cf_item->region(), true )->get_object( array( - 'Bucket' => $as3cf_item->bucket(), - 'Key' => $as3cf_item->key( $filename ), - 'SaveAs' => $file, - ) ); - } catch ( Exception $e ) { - AS3CF_Error::log( sprintf( __( 'There was an error attempting to download the file %s from the bucket: %s', 'amazon-s3-and-cloudfront' ), $as3cf_item->key( $filename ), $e->getMessage() ) ); + /** @var Download_Handler $download_handler */ + $download_handler = $this->as3cf->get_item_handler( Download_Handler::get_item_handler_key_name() ); + $result = $download_handler->handle( $as3cf_item, array( 'full_source_paths' => array( $file ) ) ); + if ( empty( $result ) || is_wp_error( $result ) ) { return false; } @@ -722,11 +688,13 @@ public function get_stream_wrapper_file( $url, $file, $attachment_id, Media_Libr public function wp_get_attachment_metadata( $data, $attachment_id ) { global $wp_current_filter; + $as3cf_item = Media_Library_Item::get_by_source_id( $attachment_id ); if ( is_array( $wp_current_filter ) && ! empty( $wp_current_filter[0] ) && 'the_content' === $wp_current_filter[0] && - $this->as3cf->is_attachment_served_by_provider( $attachment_id ) + ! empty( $as3cf_item ) && + $as3cf_item->served_by_provider( $attachment_id ) ) { // Ensure each filename is encoded the same way as URL, slightly fixed up for wp_basename() manipulation compatibility. if ( ! empty( $data['file'] ) ) { @@ -855,7 +823,8 @@ public function wp_calculate_image_srcset_meta( $image_meta, $size_array, $image return $image_meta; } - if ( ! ( $as3cf_item = $this->as3cf->is_attachment_served_by_provider( $attachment_id ) ) ) { + $as3cf_item = Media_Library_Item::get_by_source_id( $attachment_id ); + if ( ! $as3cf_item || ! $as3cf_item->served_by_provider() ) { // Attachment not uploaded to S3, abort return $image_meta; } @@ -901,7 +870,8 @@ public function wp_calculate_image_srcset( $sources, $size_array, $image_src, $i return $sources; } - if ( ! ( $as3cf_item = $this->as3cf->is_attachment_served_by_provider( $attachment_id ) ) ) { + $as3cf_item = Media_Library_Item::get_by_source_id( $attachment_id ); + if ( ! $as3cf_item || ! $as3cf_item->served_by_provider() ) { // Attachment not uploaded to S3, abort return $sources; } @@ -909,7 +879,7 @@ public function wp_calculate_image_srcset( $sources, $size_array, $image_src, $i foreach ( $sources as $width => $source ) { $filename = wp_basename( $source['url'] ); $size = $this->find_image_size_from_width( $image_meta['sizes'], $width, $filename ); - $provider_url = $this->as3cf->get_attachment_provider_url( $attachment_id, $as3cf_item, null, $size, $image_meta ); + $provider_url = $as3cf_item->get_provider_url( $size ); if ( false === $provider_url || is_wp_error( $provider_url ) ) { // Skip URLs not offloaded to S3 @@ -958,7 +928,9 @@ public function customizer_crop_unique_filename( $filename, $ext, $dir ) { // Get parent Post ID for cropped image. $post_id = filter_input( INPUT_POST, 'id', FILTER_VALIDATE_INT ); - $filename = $this->as3cf->filter_unique_filename( $filename, $ext, $dir, $post_id ); + /** @var Media_Library $media_library */ + $media_library = $this->as3cf->get_integration_manager()->get_integration( 'mlib' ); + $filename = $media_library->filter_unique_filename( $filename, $ext, $dir, $post_id ); return $filename; } @@ -1026,9 +998,7 @@ public function rest_dispatch_request_copy_back_to_local( $dispatch_result, $req if ( ! empty( $routes ) ) { foreach ( $routes as $match_route ) { if ( preg_match( '@' . $match_route . '@i', $route ) ) { - $this->enable_get_attached_file_copy_back_to_local(); - $this->wait_for_generate_attachment_metadata = true; - $this->generate_attachment_metadata_done = false; + $this->enable_copy_back_and_wait_for_generate_metadata(); break; } } diff --git a/classes/as3cf-utils.php b/classes/as3cf-utils.php index ad0f8b0a..d42c51d0 100644 --- a/classes/as3cf-utils.php +++ b/classes/as3cf-utils.php @@ -8,6 +8,7 @@ * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License */ +use DeliciousBrains\WP_Offload_Media\Items\Item; use DeliciousBrains\WP_Offload_Media\Items\Media_Library_Item; // Exit if accessed directly @@ -103,6 +104,8 @@ public static function remove_scheme( $url ) { public static function remove_size_from_filename( $url, $remove_extension = false ) { $url = preg_replace( '/^(\S+)-[0-9]{1,4}x[0-9]{1,4}(\.[a-zA-Z0-9\.]{2,})?/', '$1$2', $url ); + $url = apply_filters( 'as3cf_remove_size_from_filename', $url ); + if ( $remove_extension ) { $ext = pathinfo( $url, PATHINFO_EXTENSION ); $url = str_replace( ".$ext", '', $url ); @@ -119,7 +122,7 @@ public static function remove_size_from_filename( $url, $remove_extension = fals * @return bool */ public static function is_full_size( $size ) { - if ( empty( $size ) || in_array( $size, array( 'full', 'original' ) ) ) { + if ( empty( $size ) || in_array( $size, array( 'full', Item::primary_object_key() ) ) ) { return true; } @@ -224,7 +227,7 @@ public static function is_relative_url( $string ) { public static function get_attachment_file_paths( $attachment_id, $exists_locally = true, $meta = false, $include_backups = true ) { $file_path = get_attached_file( $attachment_id, true ); $paths = array( - 'original' => $file_path, + Item::primary_object_key() => $file_path, ); if ( ! $meta ) { @@ -240,6 +243,11 @@ public static function get_attachment_file_paths( $attachment_id, $exists_locall // If file edited, current file name might be different. if ( isset( $meta['file'] ) ) { $paths['file'] = str_replace( $file_name, wp_basename( $meta['file'] ), $file_path ); + + // However, if this file path turns out to be exactly the same as the primary objet key, we don't need it. + if ( $paths[ Item::primary_object_key() ] === $paths['file'] ) { + unset( $paths['file'] ); + } } // Thumb @@ -287,39 +295,6 @@ public static function get_attachment_file_paths( $attachment_id, $exists_locall return $paths; } - /** - * Get an attachment's edited file paths. - * - * @param int $attachment_id - * - * @return array - */ - public static function get_attachment_edited_file_paths( $attachment_id ) { - $paths = self::get_attachment_file_paths( $attachment_id, false ); - $paths = array_filter( $paths, function ( $path ) { - return preg_match( '/-e[0-9]{13}(?:-[0-9]{1,4}x[0-9]{1,4})?\./', wp_basename( $path ) ); - } ); - - return $paths; - } - - /** - * Get an attachment's edited S3 keys. - * - * @param int $attachment_id - * @param Media_Library_Item $as3cf_item - * - * @return array - */ - public static function get_attachment_edited_keys( $attachment_id, Media_Library_Item $as3cf_item ) { - $paths = self::get_attachment_edited_file_paths( $attachment_id ); - $paths = array_map( function ( $path ) use ( $as3cf_item ) { - return array( 'Key' => $as3cf_item->key( wp_basename( $path ) ) ); - }, $paths ); - - return $paths; - } - /** * Get intermediate size from attachment filename. * If multiple sizes exist with same filename, only the first size found will be returned. @@ -345,14 +320,24 @@ public static function get_intermediate_size_from_filename( $attachment_id, $fil * Strip edited image suffix and extension from path. * * @param string $path + * @param string $source_type * * @return string */ - public static function strip_image_edit_suffix_and_extension( $path ) { + public static function strip_image_edit_suffix_and_extension( $path, $source_type = 'media-library' ) { $parts = pathinfo( $path ); $filename = preg_replace( '/-e[0-9]{13}/', '', $parts['filename'] ); + $result = str_replace( $parts['basename'], $filename, $path ); - return str_replace( $parts['basename'], $filename, $path ); + /** + * Allow source type specific cleanup + * + * @param string $path + * @param string $source_type + * + * @return string + */ + return apply_filters( 'as3cf_strip_image_edit_suffix_and_extension', $result, $source_type ); } /** @@ -513,19 +498,23 @@ public static function get_first_defined_constant( $constants ) { /** * Ensure returned keys are for correct attachment. * - * @param array $keys + * @param int $source_id + * @param array $keys + * @param string $source_type * * @return array */ - public static function validate_attachment_keys( $attachment_id, $keys ) { - $paths = self::get_attachment_file_paths( $attachment_id, false ); - $filenames = array_map( 'wp_basename', $paths ); + public static function validate_attachment_keys( $source_id, $keys, $source_type ) { + if ( Media_Library_Item::source_type() === $source_type ) { + $paths = self::get_attachment_file_paths( $source_id, false ); + $filenames = array_map( 'wp_basename', $paths ); - foreach ( $keys as $key => $value ) { - $filename = wp_basename( $value ); + foreach ( $keys as $key => $value ) { + $filename = wp_basename( $value ); - if ( ! in_array( $filename, $filenames ) ) { - unset( $keys[ $key ] ); + if ( ! in_array( $filename, $filenames ) ) { + unset( $keys[ $key ] ); + } } } @@ -584,7 +573,7 @@ public static function decode_filename_in_path( $file ) { */ public static function fullsize_paths( $paths ) { if ( is_array( $paths ) && ! empty( $paths ) ) { - return array_values( array_unique( array_intersect_key( $paths, array_flip( array( 'original', 'file', 'full-orig', 'original_image' ) ) ) ) ); + return array_values( array_unique( array_intersect_key( $paths, array_flip( array( Item::primary_object_key(), 'file', 'full-orig', 'original_image' ) ) ) ) ); } else { return array(); } @@ -620,70 +609,6 @@ public static function make_upload_file_paths_relative( $paths ) { return $paths; } - /** - * Convert dimensions to size - * - * @param int $attachment_id - * @param array $dimensions - * - * @return null|string - */ - public static function convert_dimensions_to_size_name( $attachment_id, $dimensions ) { - $w = ( isset( $dimensions[0] ) && $dimensions[0] > 0 ) ? $dimensions[0] : 1; - $h = ( isset( $dimensions[1] ) && $dimensions[1] > 0 ) ? $dimensions[1] : 1; - $original_aspect_ratio = $w / $h; - $meta = wp_get_attachment_metadata( $attachment_id ); - - if ( ! isset( $meta['sizes'] ) || empty( $meta['sizes'] ) ) { - return null; - } - - $sizes = $meta['sizes']; - uasort( $sizes, function ( $a, $b ) { - // Order by image area - return ( $a['width'] * $a['height'] ) - ( $b['width'] * $b['height'] ); - } ); - - $nearest_matches = array(); - - foreach ( $sizes as $size => $value ) { - if ( $w > $value['width'] || $h > $value['height'] ) { - continue; - } - - $aspect_ratio = $value['width'] / $value['height']; - - if ( $aspect_ratio === $original_aspect_ratio ) { - return $size; - } - - $nearest_matches[] = $size; - } - - // Return nearest match - if ( ! empty( $nearest_matches ) ) { - return $nearest_matches[0]; - } - - return null; - } - - /** - * Maybe convert size to string - * - * @param int $attachment_id - * @param mixed $size - * - * @return null|string - */ - public static function maybe_convert_size_to_string( $attachment_id, $size ) { - if ( is_array( $size ) ) { - return static::convert_dimensions_to_size_name( $attachment_id, $size ); - } - - return $size; - } - /** * Encode file names according to RFC 3986 when generating urls * As per Amazon https://forums.aws.amazon.com/thread.jspa?threadID=55746#jive-message-244233 @@ -721,5 +646,18 @@ public static function encode_filename_in_path( $file ) { return str_replace( $file_name, $encoded_file_name, $file ); } + + /** + * Get a file's real mime type + * + * @param string $file_path + * + * @return string + */ + public static function get_mime_type( $file_path ) { + $file_type = wp_check_filetype_and_ext( $file_path, wp_basename( $file_path ) ); + + return $file_type['type']; + } } } diff --git a/classes/filters/as3cf-local-to-s3.php b/classes/filters/as3cf-local-to-s3.php index 45e8aa8c..93d7d8c4 100644 --- a/classes/filters/as3cf-local-to-s3.php +++ b/classes/filters/as3cf-local-to-s3.php @@ -1,6 +1,6 @@ filter_custom_css( $value, $setting->stylesheet ); @@ -102,13 +112,49 @@ public function filter_content_pagination( $pages ) { /** * Filter widget display. * - * @param array $instance - * @param WP_Widget $class + * @param array $instance * * @return array */ - public function filter_widget_display( $instance, $class ) { - return $this->handle_widget( $instance, $class ); + public function filter_widget_display( $instance ) { + return $this->handle_widget( $instance ); + } + + /** + * Filters the content of the block widget during initial load of the customizer. + * + * @param array $value The widget block. + * + * @return array + */ + public function filter_customize_value_widget_block( $value ) { + return $this->handle_widget( $value ); + } + + /** + * Filters the content of the block widget before output. + * + * @param string $content The widget content. + * + * @return string + */ + public function filter_widget_block_content( $content ) { + if ( empty( $content ) ) { + return $content; + } + + $cache = $this->get_option_cache(); + $to_cache = array(); + + $changed_content = $this->process_content( $content, $cache, $to_cache ); + + if ( ! empty( $changed_content ) && $changed_content !== $content ) { + $content = $changed_content; + } + + $this->maybe_update_option_cache( $to_cache ); + + return $content; } /** @@ -131,35 +177,61 @@ public function url_needs_replacing( $url ) { /** * Get URL * - * @param int $attachment_id - * @param null|string $size + * @param array $item_source + * @param null|string $object_key * * @return bool|string */ - protected function get_url( $attachment_id, $size = null ) { - return $this->as3cf->get_attachment_url( $attachment_id, null, $size ); + protected function get_url( $item_source, $object_key = null ) { + if ( empty( $item_source['id'] ) || empty( $item_source['source_type'] ) ) { + return false; + } + + /** + * Return the provider URL for an item + * + * @param string|false $url Url for the item, false if no URL can be determined + * @param array $item_source Associative array describing the item, guaranteed keys:- + * id: source item's unique integer id + * source_type: source item's string type identifier + * @param string|null $object_key Object key (size) describing what sub file of an item to return url for + */ + return apply_filters( 'as3cf_get_provider_url_for_item_source', false, $item_source, $object_key ); } /** * Get base URL. * - * @param int $attachment_id + * @param array $item_source * * @return string|false */ - protected function get_base_url( $attachment_id ) { - return $this->as3cf->get_attachment_local_url( $attachment_id ); + protected function get_base_url( $item_source ) { + if ( empty( $item_source['id'] ) || empty( $item_source['source_type'] ) ) { + return false; + } + + /** + * Return the local URL for an item + * + * @param string|false $url Url for the item, false if no URL can be determined + * @param array $item_source Associative array describing the item, guaranteed keys:- + * id: source item's unique integer id + * source_type: source item's string type identifier + * @param string|null $object_key Object key (size) describing what sub file of an item to return url for + */ + return apply_filters( 'as3cf_get_local_url_for_item_source', false, $item_source, null ); } /** - * Get attachment ID from URL. + * Get item source descriptor from URL. * * @param string $url * - * @return bool|int + * @return bool|array */ - public function get_attachment_id_from_url( $url ) { - $results = $this->get_attachment_ids_from_urls( array( $url ) ); + public function get_item_source_from_url( $url ) { + $results = $this->get_item_sources_from_urls( array( $url ) ); if ( empty( $results ) ) { return false; @@ -175,13 +247,13 @@ public function get_attachment_id_from_url( $url ) { } /** - * Get attachment IDs from URLs. + * Get item source descriptors from URLs. * * @param array $urls * - * @return array url => attachment ID (or false) + * @return array url => item source descriptor array (or false) */ - protected function get_attachment_ids_from_urls( $urls ) { + protected function get_item_sources_from_urls( $urls ) { $results = array(); if ( empty( $urls ) ) { @@ -226,24 +298,28 @@ protected function get_attachment_ids_from_urls( $urls ) { } if ( ! empty( $paths ) ) { - $as3cf_items = Media_Library_Item::get_by_source_path( array_keys( $paths ) ); + $as3cf_items = Item::get_by_source_path( array_keys( $paths ) ); if ( ! empty( $as3cf_items ) ) { - /* @var Media_Library_Item $as3cf_item */ + /* @var Item $as3cf_item */ foreach ( $as3cf_items as $as3cf_item ) { // Each returned item may have matched on either the source_path or original_source_path. - // Because the base image file name of a thumbnail might match the original rather scaled or rotated full image + // Because the base image file name of a thumbnail might match the primary rather scaled or rotated full image // it's possible that both source paths are used by separate URLs. foreach ( array( $as3cf_item->source_path(), $as3cf_item->original_source_path() ) as $source_path ) { if ( ! empty( $paths[ $source_path ] ) ) { $matched_full_url = $paths[ $source_path ]; if ( ! empty( $full_urls[ $matched_full_url ] ) ) { - $attachment_id = $as3cf_item->source_id(); - $this->query_cache[ $matched_full_url ] = $attachment_id; + $item_source = array( + 'id' => $as3cf_item->source_id(), + 'source_type' => $as3cf_item->source_type(), + ); + + $this->query_cache[ $matched_full_url ] = $item_source; foreach ( $full_urls[ $matched_full_url ] as $url ) { - $results[ $url ] = $attachment_id; + $results[ $url ] = $item_source; } unset( $full_urls[ $matched_full_url ] ); } @@ -252,7 +328,7 @@ protected function get_attachment_ids_from_urls( $urls ) { } } - // No more attachment IDs found, set remaining results as false. + // No more item IDs found, set remaining results as false. if ( count( $query_set ) !== count( $results ) ) { foreach ( $full_urls as $full_url => $schema_urls ) { foreach ( $schema_urls as $url ) { @@ -369,4 +445,54 @@ public function set_url_scheme( $url, $scheme, $orig_scheme ) { return $url; } + + /** + * Filters the array of queried block templates array after they've been fetched. + * + * @param WP_Block_Template[] $query_result Array of found block templates. + * @param array $query Arguments to retrieve templates. + * @param string $template_type wp_template or wp_template_part. + * + * @return WP_Block_Template[] + */ + public function filter_get_block_templates( $query_result, $query, $template_type ) { + if ( empty( $query_result ) ) { + return $query_result; + } + + foreach ( $query_result as $block_template ) { + $block_template = $this->filter_get_block_template( $block_template, $block_template->id, $template_type ); + } + + return $query_result; + } + + /** + * Filters the queried block template object after it's been fetched. + * + * @param WP_Block_Template|null $block_template The found block template, or null if there isn't one. + * @param string $id Template unique identifier (example: theme_slug//template_slug). + * @param string $template_type Template type: `'wp_template'` or '`wp_template_part'`. + * + * @return WP_Block_Template|null + */ + public function filter_get_block_template( $block_template, $id, $template_type ) { + if ( empty( $block_template ) ) { + return $block_template; + } + + $content = $block_template->content; + + if ( empty( $content ) ) { + return $block_template; + } + + $content = $this->filter_post( $content ); + + if ( ! empty( $content ) && $content !== $block_template->content ) { + $block_template->content = $content; + } + + return $block_template; + } } diff --git a/classes/filters/as3cf-s3-to-local.php b/classes/filters/as3cf-s3-to-local.php index 7416a915..ea8c1226 100644 --- a/classes/filters/as3cf-s3-to-local.php +++ b/classes/filters/as3cf-s3-to-local.php @@ -1,6 +1,6 @@ handle_widget( $instance, $class ); + public function filter_widget_save( $instance ) { + return $this->handle_widget( $instance ); + } + + /** + * Filter widget block on save. + * + * @param array $value The new, unserialized option value. + * + * @return array + */ + public function filter_widget_block_save( $value ) { + if ( empty( $value ) || ! is_array( $value ) ) { + return $value; + } + + foreach ( $value as $idx => $section ) { + $value[ $idx ] = $this->handle_widget( $section ); + } + + return $value; } /** @@ -89,54 +104,80 @@ public function url_needs_replacing( $url ) { /** * Get URL * - * @param int $attachment_id - * @param null|string $size + * @param array $item_source + * @param null|string $object_key * * @return bool|string */ - protected function get_url( $attachment_id, $size = null ) { - return $this->as3cf->get_attachment_local_url_size( $attachment_id, $size ); + protected function get_url( $item_source, $object_key = null ) { + if ( empty( $item_source['id'] ) || empty( $item_source['source_type'] ) ) { + return false; + } + + /** + * Return the local URL for an item + * + * @param string|false $url Url for the item, false if no URL can be determined + * @param array $item_source Associative array describing the item, guaranteed keys:- + * id: source item's unique integer id + * source_type: source item's string type identifier + * @param string|null $object_key Object key (size) describing what sub file of an item to return url for + */ + return apply_filters( 'as3cf_get_local_url_for_item_source', false, $item_source, $object_key ); } /** * Get base URL. * - * @param int $attachment_id + * @param array $item_source * * @return string|false */ - protected function get_base_url( $attachment_id ) { - return $this->as3cf->get_attachment_url( $attachment_id ); + protected function get_base_url( $item_source ) { + if ( empty( $item_source['id'] ) || empty( $item_source['source_type'] ) ) { + return false; + } + + /** + * Return the provider URL for an item + * + * @param string|false $url Url for the item, false if no URL can be determined + * @param array $item_source Associative array describing the item, guaranteed keys:- + * id: source item's unique integer id + * source_type: source item's string type identifier + * @param string|null $object_key Object key (size) describing what sub file of an item to return url for + */ + return apply_filters( 'as3cf_get_provider_url_for_item_source', false, $item_source, null ); } /** - * Get attachment ID from URL. + * Get item source descriptor from URL. * * @param string $url * * @return bool|int */ - public function get_attachment_id_from_url( $url ) { + public function get_item_source_from_url( $url ) { // Result for sized URL already cached in request, return it. if ( isset( $this->query_cache[ $url ] ) ) { return $this->query_cache[ $url ]; } - $post_id = Media_Library_Item::get_source_id_by_remote_url( $url ); + $item_source = Item::get_item_source_by_remote_url( $url ); - if ( $post_id ) { - $this->query_cache[ $url ] = $post_id; + if ( ! empty( $item_source['id'] ) ) { + $this->query_cache[ $url ] = $item_source; - return $post_id; + return $item_source; } $full_url = AS3CF_Utils::remove_size_from_filename( $url ); // If we've already tried to find this URL above because it didn't have a size suffix, cache and return. if ( $url === $full_url ) { - $this->query_cache[ $url ] = $post_id; + $this->query_cache[ $url ] = $item_source; - return $post_id; + return $item_source; } // Result for URL already cached in request whether found or not, return it. @@ -144,21 +185,21 @@ public function get_attachment_id_from_url( $url ) { return $this->query_cache[ $full_url ]; } - $post_id = Media_Library_Item::get_source_id_by_remote_url( $full_url ); + $item_source = Item::get_item_source_by_remote_url( $full_url ); - $this->query_cache[ $full_url ] = $post_id; + $this->query_cache[ $full_url ] = ! empty( $item_source['id'] ) ? $item_source : false; - return $post_id; + return $this->query_cache[ $full_url ]; } /** - * Get attachment IDs from URLs. + * Get item source descriptors from URLs. * * @param array $urls * - * @return array url => attachment ID (or false) + * @return array url => item source descriptor (or false) */ - protected function get_attachment_ids_from_urls( $urls ) { + protected function get_item_sources_from_urls( $urls ) { $results = array(); if ( empty( $urls ) ) { @@ -170,7 +211,7 @@ protected function get_attachment_ids_from_urls( $urls ) { } foreach ( $urls as $url ) { - $results[ $url ] = $this->get_attachment_id_from_url( $url ); + $results[ $url ] = $this->get_item_source_from_url( $url ); } return $results; @@ -219,25 +260,4 @@ protected function post_process_content( $content ) { protected function pre_replace_content( $content ) { return $content; } - - /** - * Determines if the image meta data is for the image source file. - * - * @handles wp_image_file_matches_image_meta - * - * @param bool $match - * @param string $image_location - * @param array $image_meta - * @param int $attachment_id - * - * @return bool - */ - public function image_file_matches_image_meta( $match, $image_location, $image_meta, $attachment_id ) { - // If already matched or the URL is local, there's nothing for us to do. - if ( $match || ! $this->url_needs_replacing( $image_location ) ) { - return $match; - } - - return $this->attachment_id_matches_src( $attachment_id, $image_location ); - } } diff --git a/classes/integrations/core.php b/classes/integrations/core.php new file mode 100644 index 00000000..06f61f35 --- /dev/null +++ b/classes/integrations/core.php @@ -0,0 +1,43 @@ +as3cf->get_setting( 'remove-local-file', false ) && $as3cf_item->exists_locally() ) { + $remove_local_handler = $this->as3cf->get_item_handler( Remove_Local_Handler::get_item_handler_key_name() ); + + $remove_local_handler->handle( $as3cf_item ); + } + } +} \ No newline at end of file diff --git a/classes/integrations/integration-manager.php b/classes/integrations/integration-manager.php new file mode 100644 index 00000000..fd139bd3 --- /dev/null +++ b/classes/integrations/integration-manager.php @@ -0,0 +1,81 @@ +integrations = array(); + } + + /** + * Make this class a singleton. + * + * Use this instead of __construct(). + * + * @return Integration_Manager + */ + public static function get_instance() { + if ( ! isset( static::$instance ) && ! ( self::$instance instanceof Integration_Manager ) ) { + static::$instance = new Integration_Manager(); + } + + return static::$instance; + } + + /** + * Getter for integration class instance + * + * @param string $integration_key + * + * @return Integration|null + */ + public function get_integration( $integration_key ) { + if ( ! empty( $this->integrations[ $integration_key ] ) ) { + return $this->integrations[ $integration_key ]; + } + + return null; + } + + /** + * Register integration. + * + * @param string $integration_key + * @param Integration $integration + */ + public function register_integration( $integration_key, Integration $integration ) { + if ( $integration::is_installed() ) { + $integration->init(); + } + + $this->integrations[ $integration_key ] = $integration; + } + + /** + * As this class is a singleton it should not be clone-able. + */ + protected function __clone() { + } + + /** + * As this class is a singleton it should not be able to be unserialized. + */ + public function __wakeup() { + } + +} \ No newline at end of file diff --git a/classes/integrations/integration.php b/classes/integrations/integration.php new file mode 100644 index 00000000..8536ab60 --- /dev/null +++ b/classes/integrations/integration.php @@ -0,0 +1,37 @@ +as3cf = $as3cf; + } + + /** + * Is installed? + * + * @return bool + */ + public static function is_installed() { + return false; + } + + /** + * Init integration. + */ + abstract public function init(); + +} \ No newline at end of file diff --git a/classes/integrations/media-library.php b/classes/integrations/media-library.php new file mode 100644 index 00000000..1bfaf5e6 --- /dev/null +++ b/classes/integrations/media-library.php @@ -0,0 +1,1551 @@ +as3cf->is_plugin_setup( true ) ) { + return $data; + } + + // Some other filter may already have corrupted $data + if ( is_wp_error( $data ) ) { + return $data; + } + + // Protect against updates of partially formed metadata since WordPress 5.3. + // Checks whether new upload currently has no subsizes recorded but is expected to have subsizes during upload, + // and if so, are any of its currently missing sizes part of the set. + if ( ! empty( $data ) && function_exists( 'wp_get_registered_image_subsizes' ) && function_exists( 'wp_get_missing_image_subsizes' ) ) { + + /** + * Plugin compat may require that we wait for wp_generate_attachment_metadata + * to be run before proceeding with uploading. I.e. Regenerate Thumbnails requires this. + * + * @param bool True if we should wait AND generate_attachment_metadata hasn't run yet + * + */ + if ( apply_filters( 'as3cf_wait_for_generate_attachment_metadata', false ) ) { + return $data; + } + + if ( empty( $data['sizes'] ) && wp_attachment_is_image( $post_id ) ) { + + // There is no unified way of checking whether subsizes are expected, so we have to duplicate WordPress code here. + $new_sizes = wp_get_registered_image_subsizes(); + $new_sizes = apply_filters( 'intermediate_image_sizes_advanced', $new_sizes, $data, $post_id ); + $missing_sizes = wp_get_missing_image_subsizes( $post_id ); + + if ( ! empty( $new_sizes ) && ! empty( $missing_sizes ) && array_intersect_key( $missing_sizes, $new_sizes ) ) { + return $data; + } + } + } + + // Is this a new item that we're already started working on in this request? + if ( ! empty( $this->items_in_progress[ $post_id ] ) ) { + $as3cf_item = $this->items_in_progress[ $post_id ]; + } + + // Is this an update for an existing item. + if ( empty( $as3cf_item ) || is_wp_error( $as3cf_item ) ) { + $as3cf_item = Media_Library_Item::get_by_source_id( $post_id ); + } + + // Abort if not already uploaded to provider and the copy setting is off. + if ( ! $as3cf_item && ! $this->as3cf->get_setting( 'copy-to-s3' ) ) { + return $data; + } + + if ( empty( $as3cf_item ) ) { + $as3cf_item = null; + } + + /** + * Allows implementors to cancel uploading a Media Library item for any reason. + * + * This filter is triggered by updates to an attachment's metadata. + * To potentially cancel an upload started by any method, + * please use the 'as3cf_pre_upload_item' filter. + * + * @param bool $cancel True if the upload should be cancelled + * @param array $data Array describing the object being uploaded + * @param int $post_id Attachment's ID + * @param Media_Library_Item $as3cf_item The Media Library Item object if previously offloaded + * + * @see as3cf_pre_upload_item + */ + $cancel = apply_filters( 'as3cf_pre_update_attachment_metadata', false, $data, $post_id, $as3cf_item ); + if ( false !== $cancel ) { + return $data; + } + + $offloaded_files = array(); + + // If we still don't have a valid item, create one from scratch. + if ( empty( $as3cf_item ) || is_wp_error( $as3cf_item ) ) { + $as3cf_item = Media_Library_Item::create_from_source_id( $post_id ); + } else { + $offloaded_files = $as3cf_item->offloaded_files(); + } + + // Did we get a WP_Error? + if ( is_wp_error( $as3cf_item ) ) { + AS3CF_Error::Log( $as3cf_item->get_error_message() ); + + return $data; + } + + // Or didn't we get anything at all? + if ( empty( $as3cf_item ) ) { + $message = sprintf( __( "Can't create item from media library item %d", 'amazon-s3-and-cloudfront' ), $post_id ); + AS3CF_Error::Log( $message ); + + return $data; + } + + // Update item's expected objects from attachment's new metadata. + $this->update_item_from_new_metadata( $as3cf_item, $data ); + + $this->upload_item( $as3cf_item, $offloaded_files ); + $this->items_in_progress[ $post_id ] = $as3cf_item; + + return $data; + } + + /** + * Upload item. + * + * @param Media_Library_Item $as3cf_item + * @param array $offloaded_files An array of files previously offloaded for the item. + */ + protected function upload_item( Media_Library_Item $as3cf_item, array $offloaded_files ) { + $upload_handler = $this->as3cf->get_item_handler( Upload_Handler::get_item_handler_key_name() ); + $upload_result = $upload_handler->handle( $as3cf_item, array( 'offloaded_files' => $offloaded_files ) ); + + if ( is_wp_error( $upload_result ) ) { + foreach ( $upload_result->get_error_messages() as $error_message ) { + AS3CF_Error::Log( $error_message ); + } + } + } + + /** + * Handle update_post_metadata for some media library related keys + * + * @handles update_post_metadata + * + * @param bool $check + * @param int $object_id + * @param string $meta_key + * @param mixed $meta_value + * @param mixed $prev_value + */ + public function update_post_metadata( $check, $object_id, $meta_key, $meta_value, $prev_value ) { + if ( '_wp_attachment_backup_sizes' === $meta_key ) { + if ( $this->as3cf->is_plugin_setup( true ) ) { + $this->update_attachment_backup_sizes( $object_id, $meta_value ); + } + } + + return $check; + } + + /** + * Handle updated attachment_backup_sizes. + * + * @param int $post_id + * @param array $sizes + */ + protected function update_attachment_backup_sizes( $post_id, $sizes ) { + // This item should already be known in this request, if not bail out + if ( empty( $this->items_in_progress[ $post_id ] ) ) { + return; + } + + // We should also have recorded some replaced keys in this request, if not bail + if ( empty( $this->replaced_object_keys[ $post_id ] ) ) { + return; + } + + /** @var Media_Library_Item $as3cf_item */ + $as3cf_item = $this->items_in_progress[ $post_id ]; + $existing_objects = $as3cf_item->objects(); + + foreach ( array_keys( $sizes ) as $key ) { + if ( ! isset( $existing_objects[ $key ] ) ) { + $parts = explode( '-', $key ); + $size = join( '-', array_slice( $parts, 0, -1 ) ); + if ( 'full' === $size ) { + $size = Item::primary_object_key(); + } + + if ( isset( $this->replaced_object_keys[ $post_id ][ $size ] ) ) { + $existing_objects[ $key ] = $this->replaced_object_keys[ $post_id ][ $size ]; + } + } + } + + $as3cf_item->set_objects( $existing_objects ); + $as3cf_item->save(); + } + + /** + * Filters the result when generating a unique file name. + * + * @param string $filename Unique file name. + * @param string $ext File extension, eg. ".png". + * @param string $dir Directory path. + * + * @return string + * @since 4.5.0 + * + */ + public function wp_unique_filename( $filename, $ext, $dir ) { + // Get Post ID if uploaded in post screen. + $post_id = filter_input( INPUT_POST, 'post_id', FILTER_VALIDATE_INT ); + + return $this->filter_unique_filename( $filename, $ext, $dir, $post_id ); + } + + /** + * Create unique names for file to be uploaded to AWS. + * This only applies when the remove local file option is enabled. + * + * @param string $filename Unique file name. + * @param string $ext File extension, eg. ".png". + * @param string $dir Directory path. + * @param int $post_id Attachment's parent Post ID. + * + * @return string + */ + public function filter_unique_filename( $filename, $ext, $dir, $post_id = null ) { + if ( ! $this->as3cf->is_plugin_setup( true ) ) { + return $filename; + } + + // sanitize the file name before we begin processing + $filename = sanitize_file_name( $filename ); + $ext = strtolower( $ext ); + $name = wp_basename( $filename, $ext ); + + // Edge case: if file is named '.ext', treat as an empty name. + if ( $name === $ext ) { + $name = ''; + } + + // Rebuild filename with lowercase extension as provider will have converted extension on upload. + $filename = $name . $ext; + $time = current_time( 'mysql' ); + + // Get time if uploaded in post screen. + if ( ! empty( $post_id ) ) { + $time = $this->get_post_time( $post_id ); + } + + if ( ! $this->as3cf->does_file_exist( $filename, $time ) ) { + // File doesn't exist locally or on provider, return it. + return $filename; + } + + return $this->as3cf->generate_unique_filename( $name, $ext, $time ); + } + + /** + * Allow processes to update the file on provider via update_attached_file() + * + * @param string $file + * @param int $attachment_id + * + * @return string + */ + public function update_attached_file( $file, $attachment_id ) { + if ( ! $this->as3cf->is_plugin_setup( true ) ) { + return $file; + } + + $as3cf_item = Media_Library_Item::get_by_source_id( $attachment_id ); + + if ( ! $as3cf_item ) { + return $file; + } + + /** + * Allow processes to update the file on provider via update_attached_file() + * + * @param string $file File name/path + * @param int $attachment_id Attachment id + * @param Media_Library_Item $as3cf_item The item object + */ + return apply_filters( 'as3cf_update_attached_file', $file, $attachment_id, $as3cf_item ); + } + + /** + * Removes an attachment and intermediate image size files from provider + * + * @param int $post_id + */ + public function delete_attachment( $post_id ) { + if ( ! $this->as3cf->is_plugin_setup( true ) ) { + return; + } + + $as3cf_item = Media_Library_Item::get_by_source_id( $post_id ); + + if ( ! $as3cf_item ) { + return; + } + + if ( ! $as3cf_item->served_by_provider( true ) ) { + return; + } + + // Remove the objects from the provider + $remove_provider_handler = $this->as3cf->get_item_handler( Remove_Provider_Handler::get_item_handler_key_name() ); + $remove_provider_handler->handle( $as3cf_item, array( 'verify_exists_on_local' => false ) ); + $as3cf_item->delete(); + } + + /** + * Update an existing item's expected objects from attachment's new metadata. + * + * @param Media_Library_Item $as3cf_item + * @param array $metadata + */ + protected function update_item_from_new_metadata( $as3cf_item, $metadata ) { + if ( empty( $metadata ) || ! is_array( $metadata ) ) { + return; + } + + $files = AS3CF_Utils::get_attachment_file_paths( $as3cf_item->source_id(), false, $metadata ); + $existing_basename = wp_basename( $as3cf_item->path() ); + $existing_objects = $as3cf_item->objects(); + + if ( ! isset( $this->replaced_object_keys[ $as3cf_item->source_id() ] ) ) { + $this->replaced_object_keys[ $as3cf_item->source_id() ] = array(); + } + + foreach ( $files as $object_key => $file ) { + $new_filename = wp_basename( $file ); + + if ( ! empty( $existing_objects[ $object_key ]['source_file'] ) && $existing_objects[ $object_key ]['source_file'] !== $new_filename ) { + $this->replaced_object_keys[ $as3cf_item->source_id() ][ $object_key ] = $existing_objects[ $object_key ]; + } + + if ( Item::primary_object_key() === $object_key && $existing_basename !== $new_filename ) { + $as3cf_item->set_path( str_replace( $existing_basename, $new_filename, $as3cf_item->path() ) ); + $as3cf_item->set_source_path( str_replace( $existing_basename, $new_filename, $as3cf_item->source_path() ) ); + } + + $existing_objects[ $object_key ] = array( + 'source_file' => $new_filename, + 'is_private' => isset( $existing_objects[ $object_key ]['is_private'] ) ? $existing_objects[ $object_key ]['is_private'] : false, + ); + } + + $extra_info = $as3cf_item->extra_info(); + $extra_info['objects'] = $existing_objects; + $as3cf_item->set_extra_info( $extra_info ); + } + + /** + * Load media assets. + */ + public function load_media_assets() { + $this->as3cf->enqueue_style( 'as3cf-media-styles', 'assets/css/media', array( 'as3cf-modal' ) ); + $this->as3cf->enqueue_script( 'as3cf-media-script', 'assets/js/media', array( + 'jquery', + 'media-views', + 'media-grid', + 'wp-util', + ) ); + + wp_localize_script( 'as3cf-media-script', 'as3cf_media', array( + 'strings' => $this->get_media_action_strings(), + 'nonces' => array( + 'get_attachment_provider_details' => wp_create_nonce( 'get-attachment-s3-details' ), + ), + ) ); + } + + /** + * Load the attachment assets only when editing an attachment + * + * @param $hook_suffix + */ + public function load_attachment_assets( $hook_suffix ) { + global $post; + if ( 'post.php' !== $hook_suffix || 'attachment' !== $post->post_type ) { + return; + } + + $this->as3cf->enqueue_style( 'as3cf-pro-attachment-styles', 'assets/css/attachment', array( 'as3cf-modal' ) ); + + do_action( 'as3cf_load_attachment_assets' ); + } + + /** + * Add the S3 meta box to the attachment screen + */ + public function attachment_provider_meta_box() { + add_meta_box( + 's3-actions', + __( 'Offload', 'amazon-s3-and-cloudfront' ), + array( $this, 'attachment_provider_actions_meta_box' ), + 'attachment', + 'side', + 'core' + ); + } + + /** + * Handle retrieving the provider details for attachment modals. + */ + public function ajax_get_attachment_provider_details() { + if ( ! isset( $_POST['id'] ) ) { + return; + } + + check_ajax_referer( 'get-attachment-s3-details', '_nonce' ); + + $id = intval( $_POST['id'] ); + $as3cf_item = Media_Library_Item::get_by_source_id( $id ); + $served_by_provider = false; + + if ( ! empty( $as3cf_item ) ) { + $served_by_provider = $as3cf_item->served_by_provider( true ); + } + + // get the actions available for the attachment + $data = array( + 'links' => $this->add_media_row_actions( array(), $id ), + 'provider_object' => $this->get_formatted_provider_info( $id ), + 'acl_toggle' => $this->verify_media_actions() && $served_by_provider, + ); + + wp_send_json_success( $data ); + } + + /** + * Conditionally adds copy, remove and download S3 action links for an + * attachment on the Media library list view + * + * @param array $actions + * @param WP_Post|int $post + * + * @return array + */ + public function add_media_row_actions( array $actions, $post ) { + return $actions; + } + + /** + * Get a list of available media actions which can be performed according to plugin and user capability requirements. + * + * @param string|null $scope + * + * @return array + */ + public function get_available_media_actions( $scope = '' ) { + return array(); + } + + /** + * Render the S3 attachment meta box + */ + public function attachment_provider_actions_meta_box() { + global $post; + $file = get_attached_file( $post->ID, true ); + + $args = array( + 'provider_object' => $this->get_formatted_provider_info( $post->ID ), + 'post' => $post, + 'local_file_exists' => file_exists( $file ), + 'available_actions' => $this->get_available_media_actions( 'singular' ), + 'sendback' => 'post.php?post=' . $post->ID . '&action=edit', + ); + + $this->as3cf->render_view( 'attachment-metabox', $args ); + } + + /** + * Get attachment url + * + * @param string $url + * @param int $post_id + * + * @return bool|mixed|WP_Error + */ + public function wp_get_attachment_url( $url, $post_id ) { + if ( $this->as3cf->plugin_compat->is_customizer_crop_action() ) { + return $url; + } + + $as3cf_item = Media_Library_Item::get_by_source_id( $post_id ); + if ( empty( $as3cf_item ) || ! $as3cf_item->served_by_provider() ) { + return $url; + } + + $size = $as3cf_item->get_object_key_from_filename( $url ); + $new_url = $as3cf_item->get_provider_url( $size ); + + if ( is_wp_error( $new_url ) || false === $new_url ) { + return $url; + } + + // Old naming convention, will be deprecated soon + $new_url = apply_filters( 'wps3_get_attachment_url', $new_url, $post_id, $this ); + + /** + * Filter the rewritten provider URL for a Media Library Item (attachment) + * + * @param string $url The URL + * @param int $post_id Attachment post id + */ + return apply_filters( 'as3cf_wp_get_attachment_url', $new_url, $post_id ); + } + + /** + * Return a formatted provider info array with display friendly defaults + * + * @param int $id + * + * @return bool|array + */ + public function get_formatted_provider_info( $id ) { + $as3cf_item = Media_Library_Item::get_by_source_id( $id ); + + if ( ! $as3cf_item ) { + return false; + } + + $provider_object = $as3cf_item->key_values(); + + // Backwards compatibility. + $provider_object['key'] = $provider_object['path']; + $provider_object['url'] = $as3cf_item->get_provider_url(); + + $acl = $as3cf_item->is_private() ? $this->as3cf->get_storage_provider()->get_private_acl() : $this->as3cf->get_storage_provider()->get_default_acl(); + $acl_info = array( + 'acl' => $acl, + 'name' => $this->as3cf->get_acl_display_name( $acl ), + 'title' => $this->get_media_action_strings( 'change_to_private' ), + ); + + if ( $as3cf_item->is_private() ) { + $acl_info['title'] = $this->get_media_action_strings( 'change_to_public' ); + } + + $provider_object['acl'] = $acl_info; + $provider_object['region'] = $this->as3cf->get_storage_provider()->get_region_name( $provider_object['region'] ); + $provider_object['provider_name'] = $this->as3cf->get_provider_service_name( $provider_object['provider'] ); + + return $provider_object; + } + + /** + * Filters the list of attachment image attributes. + * + * @param array $attr Attributes for the image markup. + * @param WP_Post $attachment Image attachment post. + * @param string|array $size Requested size. Image size or array of width and height values (in that order). + * + * @return array + */ + public function wp_get_attachment_image_attributes( $attr, $attachment, $size ) { + $as3cf_item = Media_Library_Item::get_by_source_id( $attachment->ID ); + if ( ! $as3cf_item || ! $as3cf_item->served_by_provider() ) { + return $attr; + } + + $size = $this->maybe_convert_size_to_string( $attachment->ID, $size ); + + // image_downsize incorrectly substitutes size filename into full URL for src attribute instead of clobbering. + // So we need to fix up the src attribute if a size is being used. + if ( ! empty( $size ) && ! empty( $attr['src'] ) ) { + $attr['src'] = $as3cf_item->get_provider_url( $size ); + } + + /** + * Filtered list of attachment image attributes. + * + * @param array $attr Attributes for the image markup. + * @param WP_Post $attachment Image attachment post. + * @param string $size Requested size. + * @param Media_Library_Item $as3cf_item + */ + return apply_filters( 'as3cf_wp_get_attachment_image_attributes', $attr, $attachment, $size, $as3cf_item ); + } + + /** + * Maybe encode attachment URLs when retrieving the image tag + * + * @param string $html + * @param int $id + * @param string $alt + * @param string $title + * @param string $align + * @param string $size + * + * @return string + */ + public function maybe_encode_get_image_tag( $html, $id, $alt, $title, $align, $size ) { + $as3cf_item = Media_Library_Item::get_by_source_id( $id ); + if ( ! $as3cf_item || ! $as3cf_item->served_by_provider() ) { + return $html; + } + + if ( ! is_string( $html ) ) { + return $html; + } + + preg_match( '@\ssrc=[\'\"]([^\'\"]*)[\'\"]@', $html, $matches ); + + if ( ! isset( $matches[1] ) ) { + // Can't establish img src + return $html; + } + + $img_src = $matches[1]; + $new_img_src = $this->maybe_sign_intermediate_size( $img_src, $id, $size, $as3cf_item ); + $new_img_src = AS3CF_Utils::encode_filename_in_path( $new_img_src ); + + return str_replace( $img_src, $new_img_src, $html ); + } + + /** + * Maybe encode URLs for images that represent an attachment + * + * @param array|bool $image + * @param int $attachment_id + * @param string|array $size + * @param bool $icon + * + * @return array + */ + public function maybe_encode_wp_get_attachment_image_src( $image, $attachment_id, $size, $icon ) { + $as3cf_item = Media_Library_Item::get_by_source_id( $attachment_id ); + if ( ! $as3cf_item || ! $as3cf_item->served_by_provider() ) { + return $image; + } + + if ( isset( $image[0] ) ) { + $url = $this->maybe_sign_intermediate_size( $image[0], $attachment_id, $size, $as3cf_item ); + $url = AS3CF_Utils::encode_filename_in_path( $url ); + + $image[0] = $url; + } + + return $image; + } + + /** + * Maybe encode URLs when outputting attachments in the media grid + * + * @param array $response + * @param int|object $attachment + * @param array $meta + * + * @return array + */ + public function maybe_encode_wp_prepare_attachment_for_js( $response, $attachment, $meta ) { + $as3cf_item = Media_Library_Item::get_by_source_id( $attachment->ID ); + if ( empty( $as3cf_item ) || ! $as3cf_item->served_by_provider() ) { + return $response; + } + + if ( isset( $response['url'] ) ) { + $response['url'] = AS3CF_Utils::encode_filename_in_path( $response['url'] ); + } + + if ( isset( $response['sizes'] ) && is_array( $response['sizes'] ) ) { + foreach ( $response['sizes'] as $size => $value ) { + $url = $this->maybe_sign_intermediate_size( $value['url'], $attachment->ID, $size, $as3cf_item, true ); + $url = AS3CF_Utils::encode_filename_in_path( $url ); + + $response['sizes'][ $size ]['url'] = $url; + } + } + + return $response; + } + + /** + * Maybe encode URLs when retrieving intermediate sizes. + * + * @param array $data + * @param int $post_id + * @param string|array $size + * + * @return array + */ + public function maybe_encode_image_get_intermediate_size( $data, $post_id, $size ) { + $as3cf_item = Media_Library_Item::get_by_source_id( $post_id ); + if ( ! $as3cf_item || ! $as3cf_item->served_by_provider() ) { + return $data; + } + + if ( isset( $data['url'] ) ) { + $url = $this->maybe_sign_intermediate_size( $data['url'], $post_id, $size, $as3cf_item ); + $url = AS3CF_Utils::encode_filename_in_path( $url ); + + $data['url'] = $url; + } + + return $data; + } + + /** + * Sign intermediate size. + * + * @param string $url + * @param int $attachment_id + * @param string|array $size + * @param bool|Media_Library_Item $as3cf_item + * @param bool $force_rewrite If size not signed, make sure correct URL is being used anyway. + * + * @return string|WP_Error + */ + protected function maybe_sign_intermediate_size( $url, $attachment_id, $size, $as3cf_item = false, $force_rewrite = false ) { + if ( ! $as3cf_item ) { + $as3cf_item = Media_Library_Item::get_by_source_id( $attachment_id ); + } + + $size = $this->maybe_convert_size_to_string( $attachment_id, $size ); + + if ( $force_rewrite || $as3cf_item->is_private( $size ) ) { + // Private file, add AWS signature if required + return $as3cf_item->get_provider_url( $size ); + } + + return $url; + } + + /** + * Return the provider URL when the local file is missing + * unless we know who the calling process is, and we are happy + * to copy the file back to the server to be used. + * + * @handles get_attached_file + * @handles wp_get_original_image_path + * + * @param string $file + * @param int $attachment_id + * + * @return string + */ + public function get_attached_file( $file, $attachment_id ) { + // During the deletion of an attachment, stream wrapper URLs should not be returned. + if ( $this->deleting_attachment ) { + return $file; + } + + $as3cf_item = Media_Library_Item::get_by_source_id( $attachment_id ); + if ( ! empty( $as3cf_item ) && ! $as3cf_item->served_by_provider() ) { + $as3cf_item = false; + } + + if ( file_exists( $file ) || ! $as3cf_item ) { + if ( $as3cf_item ) { + /** + * This filter gives filter implementors a chance to copy back siblings for + * a local file even if the main already exists locally. + * + * @param string $url Item URL + * @param string $file Local file path + * @param int $attachment_id Attachment post id + * @param Media_Library_Item $as3cf_item The Item object + * + */ + return apply_filters( 'as3cf_get_attached_file_noop', $file, $file, $attachment_id, $as3cf_item ); + } else { + return $file; + } + } + + $url = $as3cf_item->get_provider_url(); + if ( false === $url || is_wp_error( $url ) ) { + return $file; + } + + /** + * This filter gives filter implementors a chance to copy back missing item files + * from the provider before WordPress returns the file name/path for it. Defaults to + * returning the remote URL. + * + * @param string $url Item URL + * @param string $file Local file path + * @param int $attachment_id Attachment post id + * @param Media_Library_Item $as3cf_item The Item object + * + */ + return apply_filters( 'as3cf_get_attached_file', $url, $file, $attachment_id, $as3cf_item ); + } + + /** + * Filters the audio & video shortcodes output to remove "&_=NN" params from source.src as it breaks signed URLs. + * + * @param string $html Shortcode HTML output. + * @param array $atts Array of shortcode attributes. + * @param string $media Media file. + * @param int $post_id Post ID. + * @param string $library Media library used for the shortcode. + * + * @return string + * + * Note: Depends on 30377.4.diff from https://core.trac.wordpress.org/ticket/30377 + */ + public function wp_media_shortcode( $html, $atts, $media, $post_id, $library ) { + return preg_replace( '/&_=[0-9]+/', '', $html ); + } + + /** + * Check we can do the media actions + * + * @return bool + */ + public function verify_media_actions() { + return false; + } + + /** + * Get all strings or a specific string used for the media actions + * + * @param null|string $string + * + * @return array|string + */ + public function get_media_action_strings( $string = null ) { + $not_verified_value = __( 'No', 'amazon-s3-and-cloudfront' ); + $not_verified_value .= ' '; + $not_verified_value .= $this->as3cf->more_info_link( '/wp-offload-media/doc/add-metadata-tool/', 'os3+attachment+metabox', 'analyze-and-repair', 'More Info', '(', ')' ); + + /** + * Returns all strings used to render meta boxes on the WordPress Media Library edit page + * + * @param array $strings Associative array of strings + */ + $strings = apply_filters( 'as3cf_media_action_strings', array( + 'provider' => _x( 'Storage Provider', 'Storage provider key name', 'amazon-s3-and-cloudfront' ), + 'provider_name' => _x( 'Storage Provider', 'Storage provider name', 'amazon-s3-and-cloudfront' ), + 'bucket' => _x( 'Bucket', 'Bucket name', 'amazon-s3-and-cloudfront' ), + 'key' => _x( 'Path', 'Path to file in bucket', 'amazon-s3-and-cloudfront' ), + 'region' => _x( 'Region', 'Location of bucket', 'amazon-s3-and-cloudfront' ), + 'acl' => _x( 'Access', 'Access control list of the file in bucket', 'amazon-s3-and-cloudfront' ), + 'url' => __( 'URL', 'amazon-s3-and-cloudfront' ), + 'is_verified' => _x( 'Verified', 'Whether or not metadata has been verified', 'amazon-s3-and-cloudfront' ), + 'not_verified' => $not_verified_value, + ) ); + + if ( ! is_null( $string ) ) { + return isset( $strings[ $string ] ) ? $strings[ $string ] : ''; + } + + return $strings; + } + + /** + * Remove 'filesize' from attachment's metadata if appropriate, also our total filesize record. + * + * @param int $post_id Attachment's post_id. + * @param array $data Attachment's metadata. + * @param bool $update_metadata Update the metadata record now? Defaults to true. + * + * @return array Attachment's cleaned up metadata. + */ + public function maybe_cleanup_filesize_metadata( $post_id, $data, $update_metadata = true ) { + if ( ! is_int( $post_id ) || empty( $post_id ) || empty( $data ) || ! is_array( $data ) ) { + return $data; + } + + /* + * Audio and video have a filesize added to metadata by default, but images and anything else don't. + * Note: Could have used `wp_generate_attachment_metadata` here to test whether default metadata has 'filesize', + * but it not only has side effects it also does a lot of work considering it's not a huge deal for this entry to hang around. + */ + if ( + empty( $data['mime_type'] ) || + 0 === strpos( $data['mime_type'], 'image/' ) || + ! ( 0 === strpos( $data['mime_type'], 'audio/' ) || 0 === strpos( $data['mime_type'], 'video/' ) ) + ) { + unset( $data['filesize'] ); + } + + if ( $update_metadata ) { + if ( empty( $data ) ) { + delete_post_meta( $post_id, '_wp_attachment_metadata' ); + } else { + update_post_meta( $post_id, '_wp_attachment_metadata', $data ); + } + } + + delete_post_meta( $post_id, 'as3cf_filesize_total' ); + + return $data; + } + + /** + * Get ACL value string. + * + * @param array $acl + * @param int $post_id + * + * @return string + */ + public function get_acl_value_string( $acl, $post_id ) { + return $acl['name']; + } + + /** + * Determines if the image metadata is for the image source file. + * + * @handles wp_image_file_matches_image_meta + * + * @param bool $match + * @param string $image_location + * @param array $image_meta + * @param int $source_id + * + * @return bool + */ + public function image_file_matches_image_meta( $match, $image_location, $image_meta, $source_id ) { + // If already matched or the URL is local, there's nothing for us to do. + if ( $match || ! $this->as3cf->filter_local->url_needs_replacing( $image_location ) ) { + return $match; + } + + $item = array( + 'id' => $source_id, + 'source_type' => Media_Library_Item::source_type(), + ); + + return $this->as3cf->filter_local->item_matches_src( $item, $image_location ); + } + + /** + * Get the local URL for a Media Library Item + * + * @handles as3cf_get_local_url_for_item_source + * + * @param string $url Url + * @param array $item_source The item source descriptor array + * @param string $size Name of requested size + * + * @return string|false + */ + public function filter_get_local_url_for_item_source( $url, $item_source, $size ) { + if ( Media_Library_Item::source_type() !== $item_source['source_type'] ) { + return $url; + } + + $as3cf_item = Media_Library_Item::get_by_source_id( $item_source['id'] ); + if ( ! empty( $as3cf_item ) ) { + return $as3cf_item->get_local_url( $size ); + } + + return $url; + } + + /** + * Get the remote URL for a Media Library Item + * + * @handles as3cf_get_provider_url_for_item_source + * + * @param string $url Url + * @param array $item_source The item source descriptor array + * @param string $size Name of requested size + * + * @return string|false + */ + public function filter_get_provider_url_for_item_source( $url, $item_source, $size ) { + if ( Media_Library_Item::source_type() !== $item_source['source_type'] ) { + return $url; + } + + $as3cf_item = Media_Library_Item::get_by_source_id( $item_source['id'] ); + if ( empty( $as3cf_item ) ) { + return $url; + } + + $url = $as3cf_item->get_provider_url( $size ); + + if ( is_wp_error( $url ) ) { + return false; + } + + return $url; + } + + /** + * Get the size from a URL for media library item types + * + * @handles as3cf_get_size_string_from_url_for_item_source + * + * @param string $size + * @param string $url + * @param array $item_source + * + * @return string + */ + public function get_size_string_from_url_for_item_source( $size, $url, $item_source ) { + if ( Media_Library_Item::source_type() !== $item_source['source_type'] ) { + return $size; + } + + $meta = get_post_meta( $item_source['id'], '_wp_attachment_metadata', true ); + + if ( empty( $meta['sizes'] ) ) { + // No alternative sizes available, return + return $size; + } + + $basename = AS3CF_Utils::encode_filename_in_path( wp_basename( $this->as3cf->maybe_remove_query_string( $url ) ) ); + + foreach ( $meta['sizes'] as $size_name => $file ) { + if ( $basename === AS3CF_Utils::encode_filename_in_path( $file['file'] ) ) { + return $size_name; + } + } + + return $size; + } + + /** + * Get attachment id from remote URL. + * + * @param string $url + * + * @return bool|int + */ + public function get_attachment_id_from_provider_url( $url ) { + $item_source = $this->as3cf->filter_provider->get_item_source_from_url( $url ); + + if ( ! empty( $item_source['id'] ) && ! empty( $item_source['source_type'] ) && Media_Library_Item::source_type() === $item_source['source_type'] ) { + return $item_source['id']; + } + + return false; + } + + /** + * Get attachment id from local URL. + * + * @param string $url + * + * @return bool|int + */ + public function get_attachment_id_from_local_url( $url ) { + $item_source = $this->as3cf->filter_local->get_item_source_from_url( $url ); + + if ( ! empty( $item_source['id'] ) && ! empty( $item_source['source_type'] ) && Media_Library_Item::source_type() === $item_source['source_type'] ) { + return $item_source['id']; + } + + return false; + } + + /** + * Get post time + * + * @param int $post_id + * + * @return string + */ + private function get_post_time( $post_id ) { + $time = current_time( 'mysql' ); + + if ( ! $post = get_post( $post_id ) ) { + return $time; + } + + if ( substr( $post->post_date, 0, 4 ) > 0 ) { + $time = $post->post_date; + } + + return $time; + } + + /** + * Maybe convert size to string + * + * @param int $attachment_id + * @param mixed $size + * + * @return null|string + */ + private function maybe_convert_size_to_string( $attachment_id, $size ) { + if ( is_array( $size ) ) { + return $this->convert_dimensions_to_size_name( $attachment_id, $size ); + } + + return $size; + } + + /** + * Convert dimensions to size + * + * @param int $attachment_id + * @param array $dimensions + * + * @return null|string + */ + private function convert_dimensions_to_size_name( $attachment_id, $dimensions ) { + $w = ( isset( $dimensions[0] ) && $dimensions[0] > 0 ) ? $dimensions[0] : 1; + $h = ( isset( $dimensions[1] ) && $dimensions[1] > 0 ) ? $dimensions[1] : 1; + $original_aspect_ratio = $w / $h; + $meta = wp_get_attachment_metadata( $attachment_id ); + + if ( ! isset( $meta['sizes'] ) || empty( $meta['sizes'] ) ) { + return null; + } + + $sizes = $meta['sizes']; + uasort( $sizes, function ( $a, $b ) { + // Order by image area + return ( $a['width'] * $a['height'] ) - ( $b['width'] * $b['height'] ); + } ); + + $nearest_matches = array(); + + foreach ( $sizes as $size => $value ) { + if ( $w > $value['width'] || $h > $value['height'] ) { + continue; + } + + $aspect_ratio = $value['width'] / $value['height']; + + if ( $aspect_ratio === $original_aspect_ratio ) { + return $size; + } + + $nearest_matches[] = $size; + } + + // Return nearest match + if ( ! empty( $nearest_matches ) ) { + return $nearest_matches[0]; + } + + return null; + } + + /** + * Has the given attachment been uploaded by this instance? + * + * @param int $source_id + * + * @return bool + */ + public function item_just_uploaded( $source_id ) { + if ( is_int( $source_id ) && isset( $this->items_in_progress[ $source_id ] ) ) { + return true; + } + + return false; + } + + /** + * Call legacy attachment specific version of the as3cf_get_item_secure_url filter. + * + * @param string $url The URL + * @param Item $as3cf_item The Item object + * @param array $item_source The item source descriptor array + * @param int $timestamp Expiry timestamp + * @param array $headers Optional extra http headers + * + * @handles as3cf_get_item_secure_url + * + * @return string|mixed + */ + public function get_item_secure_url( $url, $as3cf_item, $item_source, $timestamp, $headers ) { + if ( Media_Library_Item::source_type() !== $item_source['source_type'] ) { + return $url; + } + + /** + * Filters the secure url for an attachment + * + * @param string $url The URL + * @param Item $as3cf_item The Item object + * @param int $id The attachment id + * @param int $timestamp Expiry timestamp + * @param array $headers Optional extra http headers + * + * @deprecated 2.6.0 Please use filter "as3cf_get_item_secure_url" instead. + */ + return apply_filters( 'as3cf_get_attachment_secure_url', $url, $as3cf_item, $item_source['id'], $timestamp, $headers ); + } + + /** + * Call legacy attachment specific version of the as3cf_get_item_url filter. + * + * @param string $url The URL + * @param Item $as3cf_item The Item object + * @param array $item_source The item source descriptor array + * @param int $timestamp Expiry timestamp + * @param array $headers Optional extra http headers + * + * @handles as3cf_get_item_url + * + * @return string|mixed + */ + public function get_item_url( $url, $as3cf_item, $item_source, $timestamp, $headers ) { + if ( Media_Library_Item::source_type() !== $item_source['source_type'] ) { + return $url; + } + + /** + * Filters the url for an attachment + * + * @param string $url The URL + * @param Item $as3cf_item The Item object + * @param int $id The attachment id + * @param int $timestamp Expiry timestamp + * @param array $headers Optional extra http headers + * + * @deprecated 2.6.0 Please use filter "as3cf_get_item_url" instead. + */ + return apply_filters( 'as3cf_get_attachment_url', $url, $as3cf_item, $item_source['id'], $timestamp, $headers ); + } + + /** + * Call legacy attachment specific version of the as3cf_remove_source_files_from_provider filter. + * + * @param array $paths Array of local paths to be removed from provider + * @param Item $as3cf_item The Item object + * @param array $item_source The item source descriptor array + * + * @handles as3cf_remove_source_files_from_provider + * + * @return array|mixed + */ + public function filter_remove_source_files_from_provider( $paths, Item $as3cf_item, $item_source ) { + if ( Media_Library_Item::source_type() !== $item_source['source_type'] ) { + return $paths; + } + + /** + * Filters which provider files to remove + * + * @param array $paths Array of local paths to be removed from provider + * @param int $id Item attachment id + * @param Item $as3cf_item The Item + * @param bool $include_backups Also include backup files? + * + * @deprecated 2.6.0 Please use filter "as3cf_remove_source_files_from_provider" instead. + */ + return apply_filters( 'as3cf_remove_attachment_paths', $paths, $item_source['id'], $as3cf_item, true ); + } + + /** + * Calls legacy attachment specific version of as3cf_remove_local_files filter. + * + * @param array $files_to_remove Array of paths to be removed + * @param Item $as3cf_item The Item object + * @param array $item_source The item source descriptor array + * + * @handles as3cf_remove_local_files + * + * @return array|mixed + */ + public function filter_remove_local_files( $files_to_remove, $as3cf_item, $item_source ) { + if ( Media_Library_Item::source_type() !== $item_source['source_type'] ) { + return $files_to_remove; + } + + /** + * Filters which local files should be removed. + * + * @param array $paths Paths that will be removed + * @param int $id Attachment id + * @param string $source_path Path to primary file + * + * @deprecated 2.6.0 Please use filter "as3cf_remove_local_files" instead. + */ + return apply_filters( 'as3cf_upload_attachment_local_files_to_remove', $files_to_remove, $item_source['id'], $as3cf_item->full_source_path( Item::primary_object_key() ) ); + } + + /** + * Handle post upload duties if uploaded item is a media-library item. + * + * @handles as3cf_post_upload_item + * + * @param Media_Library_Item $as3cf_item + */ + public function post_upload_item( $as3cf_item ) { + if ( Media_Library_Item::source_type() !== $as3cf_item->source_type() ) { + return; + } + + // Make sure duplicates are marked as offloaded too. + $as3cf_item->offload_duplicate_items(); + + /** + * Fires after an attachment has been uploaded to the provider. + * + * @param int $id Attachment id + * @param Item $as3cf_item The item that was just uploaded + * + * @deprecated 2.6.0 Please use action "as3cf_post_upload_item" instead. + */ + do_action( 'as3cf_post_upload_attachment', $as3cf_item->source_id(), $as3cf_item ); + } + + /** + * Call legacy media library specific filter for cancelling an upload. + * + * @param bool $cancel Should the action on the item be cancelled? + * @param Item $as3cf_item The item that the action is being handled for. + * @param array $options Handler dependent options that may have been set for the action. + * + * @handles as3cf_pre_handle_item_upload + * + * @return bool + */ + public function pre_handle_item_upload( $cancel, $as3cf_item, array $options ) { + if ( Media_Library_Item::source_type() !== $as3cf_item->source_type() ) { + return $cancel; + } + + // Get unfiltered attachment metadata to pass into legacy filter. + $metadata = wp_get_attachment_metadata( $as3cf_item->source_id(), true ); + if ( is_wp_error( $metadata ) ) { + return $metadata; + } + + /** + * Allow provider upload to be cancelled for any reason. + * + * @param bool $cancel Should the upload for the attachment be cancelled? + * @param int $id Attachment id + * @param array $metadata Attachment metadata + * + * @deprecated 2.6.0 Please use filter "as3cf_pre_upload_item" instead. + */ + return apply_filters( 'as3cf_pre_upload_attachment', $cancel, $as3cf_item->source_id(), $metadata ); + } + + /** + * Call legacy filter for determining private status on an item's individual object_key. + * + * @param bool $is_private + * @param string $object_key + * @param Item $as3cf_item + * + * @handles as3cf_upload_object_key_as_private + * + * @return bool + */ + public function filter_upload_object_key_as_private( $is_private, $object_key, $as3cf_item ) { + if ( Media_Library_Item::source_type() !== $as3cf_item->source_type() ) { + return $is_private; + } + + $metadata = wp_get_attachment_metadata( $as3cf_item->source_id(), true ); + $default_acl = $this->as3cf->get_storage_provider()->get_default_acl(); + $private_acl = $this->as3cf->get_storage_provider()->get_private_acl(); + $acl = true === $is_private ? $private_acl : $default_acl; + + if ( Item::primary_object_key() === $object_key ) { + $file_name = wp_basename( $as3cf_item->source_path() ); + $file_type = wp_check_filetype_and_ext( $as3cf_item->source_path(), $file_name ); + + // Old naming convention, will be removed soon. + $acl = apply_filters( 'wps3_upload_acl', $acl, $file_type['type'], $metadata, $as3cf_item->source_id(), $this->as3cf ); + + /** + * Determine canned ACL for an item's original (full size) file about to be uploaded to provider. + * + * @param string $acl The canned ACL for the provider. + * @param array $metadata The attachment's metadata. + * @param int $id The attachment's ID. + * + * @deprecated 2.6.0 Please use filter "as3cf_upload_object_key_as_private" instead. + */ + $acl = apply_filters( 'as3cf_upload_acl', $acl, $metadata, $as3cf_item->source_id() ); + } else { + /** + * Determine ACL for an item's individual thumbnail size about to be uploaded to provider. + * + * @param string $acl The canned ACL for the provider. + * @param string $size Size name for file (thumbnail, medium, large). + * @param int $id The attachment's ID. + * @param array $data The attachment's metadata. + * + * @deprecated 2.6.0 Please use filter "as3cf_upload_object_key_as_private" instead. + */ + $acl = apply_filters( 'as3cf_upload_acl_sizes', $acl, $object_key, $as3cf_item->source_id(), $metadata ); + } + + if ( ! empty( $acl ) && $private_acl === $acl ) { + return true; + } + + return $is_private; + } + + /** + * Fire legacy action just before a Media Library Item is offloaded. + * + * @handles as3cf_pre_upload_object + * + * @param Item $as3cf_item + * @param array $args + */ + public function action_pre_upload_object( $as3cf_item, $args ) { + if ( Media_Library_Item::source_type() !== $as3cf_item->source_type() ) { + return; + } + + /** + * Actions fires when an Item's original file might be offloaded. + * + * This action gives notice that an Item is being processed for upload to a bucket, + * and the given arguments represent the original file's potential offload location. + * However, if the current process is for picking up extra files associated with the item, + * the indicated original file may not actually be offloaded if it does not exist + * on the server but has already been offloaded. + * + * + * @param int $id The attachment id. + * @param Media_Library_Item $as3cf_item The Item whose files are being offloaded. + * @param string $path The path to the item. + * @param array $args The arguments that could be used to offload the original file. + * + * @deprecated 2.6.0 Please use action "as3cf_pre_upload_object" instead. + */ + do_action( 'as3cf_upload_attachment_pre_remove', $as3cf_item->source_id(), $as3cf_item, $as3cf_item->normalized_path_dir(), $args ); + } + + /** + * Takes notice that an attachment is about to be deleted and prepares for it. + * + * @handles pre_delete_attachment + * + * @param bool|null $delete Whether to go forward with deletion. + * + * @return bool|null + */ + public function pre_delete_attachment( $delete ) { + if ( is_null( $delete ) ) { + $this->deleting_attachment = true; + } + + return $delete; + } + + /** + * Takes notice that an attachment has been deleted and undoes previous preparations for the event. + * + * @handles delete_post + * + * Note: delete_post is used as there is a potential that deleted_post is not reached. + */ + public function delete_post() { + $this->deleting_attachment = false; + } +} diff --git a/classes/items/download-handler.php b/classes/items/download-handler.php new file mode 100644 index 00000000..e823b217 --- /dev/null +++ b/classes/items/download-handler.php @@ -0,0 +1,169 @@ + array(), + ); + } + + /** + * Prepare a manifest based on the item. + * + * @param Item $as3cf_item + * @param array $options + * + * @return Manifest + */ + protected function pre_handle( Item $as3cf_item, array $options ) { + $manifest = new Manifest(); + $file_paths = array(); + + foreach ( $as3cf_item->objects() as $object_key => $object ) { + $file = $as3cf_item->full_source_path( $object_key ); + + if ( 0 < count( $options['full_source_paths'] ) && ! in_array( $file, $options['full_source_paths'] ) ) { + continue; + } + + $file_paths[ $object_key ] = $file; + } + + $file_paths = array_unique( $file_paths ); + + foreach ( $file_paths as $object_key => $file_path ) { + if ( ! file_exists( $file_path ) ) { + $manifest->objects[] = array( + 'args' => array( + 'Bucket' => $as3cf_item->bucket(), + 'Key' => $as3cf_item->provider_key( $object_key ), + 'SaveAs' => $file_path, + ), + ); + } + } + + return $manifest; + } + + /** + * Perform the downloads. + * + * @param Item $as3cf_item + * @param Manifest $manifest + * @param array $options + * + * @return boolean|WP_Error + * @throws Exception + */ + protected function handle_item( Item $as3cf_item, Manifest $manifest, array $options ) { + if ( ! empty( $manifest->objects ) ) { + // This test is "late" so that we don't raise the error if the local files exist anyway. + // If the provider of this item is different from what's currently configured, + // we'll return an error. + $current_provider = $this->as3cf->get_storage_provider(); + if ( ! empty( $current_provider ) && $current_provider::get_provider_key_name() !== $as3cf_item->provider() ) { + $message = sprintf( + __( '%1$s with ID %d is offloaded to a different provider than currently configured', 'amazon-s3-and-cloudfront' ), + $this->as3cf->get_source_type_name( $as3cf_item->source_type() ), + $as3cf_item->source_id() + ); + + return new WP_Error( 'exception', $message ); + } else { + $provider_client = $this->as3cf->get_provider_client( $as3cf_item->region() ); + + foreach ( $manifest->objects as &$manifest_object ) { + // Save object to a file. + $result = $this->download_object( $provider_client, $manifest_object['args'] ); + + $manifest_object['download_result']['status'] = self::STATUS_OK; + + if ( is_wp_error( $result ) ) { + $manifest_object['download_result']['status'] = self::STATUS_FAILED; + $manifest_object['download_result']['message'] = $result->get_error_message(); + } + } + } + } + + return true; + } + + /** + * Perform post handle tasks. Log errors, update filesize totals etc. + * + * @param Item $as3cf_item + * @param Manifest $manifest + * @param array $options + * + * @return bool|WP_Error + */ + protected function post_handle( Item $as3cf_item, Manifest $manifest, array $options ) { + // Look for errors + $errors = new WP_Error; + $i = 1; + + foreach ( $manifest->objects as $manifest_object ) { + if ( $manifest_object['download_result']['status'] !== self::STATUS_OK ) { + $errors->add( 'download-error-' . $i++, $manifest_object['download_result']['message'] ); + } + } + + if ( count( $errors->get_error_codes() ) ) { + return $errors; + } + + $as3cf_item->update_filesize_after_download_local(); + + return true; + } + + /** + * Download an object from provider. + * + * @param Storage_Provider $provider_client + * @param array $object + * + * @return bool|WP_Error + */ + private function download_object( $provider_client, $object ) { + // Make sure the local directory exists. + $dir = dirname( $object['SaveAs'] ); + if ( ! is_dir( $dir ) && ! wp_mkdir_p( $dir ) ) { + $error_message = sprintf( __( 'The local directory %s does not exist and could not be created.', 'amazon-s3-and-cloudfront' ), $dir ); + AS3CF_Error::log( sprintf( __( 'There was an error attempting to download the file %s from the bucket: %s', 'amazon-s3-and-cloudfront' ), $object['Key'], $error_message ) ); + } + + try { + $provider_client->get_object( $object ); + } catch ( Exception $e ) { + $error_msg = sprintf( __( 'Error downloading %1$s from bucket: %2$s', 'amazon-s3-and-cloudfront' ), $object['Key'], $e->getMessage() ); + AS3CF_Error::log( $error_msg ); + + // If storage provider file doesn't exist, an empty local file will be created, clean it up. + @unlink( $object['SaveAs'] ); + + return new WP_Error( 'download_object', $error_msg ); + } + + return true; + } +} \ No newline at end of file diff --git a/classes/items/item-handler.php b/classes/items/item-handler.php new file mode 100644 index 00000000..df48e8db --- /dev/null +++ b/classes/items/item-handler.php @@ -0,0 +1,247 @@ +as3cf = $as3cf; + } + + /** + * Get the item handler key name. + * + * @return string + */ + public static function get_item_handler_key_name() { + return static::$item_handler_key; + } + + /** + * The default options that should be used if none supplied. + * + * @return array + */ + public static function default_options() { + return array(); + } + + /** + * Main entrypoint for handling an item. + * + * @param Item $as3cf_item + * @param array $options + * + * @return boolean|WP_Error + */ + public function handle( Item $as3cf_item, array $options = array() ) { + // Merge supplied option values into the defaults as long as supplied options are recognised. + if ( empty( $options ) || ! is_array( $options ) ) { + $options = array(); + } + $options = array_merge( $this->default_options(), array_intersect_key( $options, $this->default_options() ) ); + + try { + /** + * Filter fires before handling an action on an item, allows action to be cancelled. + * + * This is a generic handler filter that includes the handler's key name as the last param. + * + * @param bool $cancel Should the action on the item be cancelled? + * @param Item $as3cf_item The item that the action is being handled for. + * @param array $options Handler dependent options that may have been set for the action. + * @param array $handler_key_name The handler's key name as per `Item_Handler::get_item_handler_key_name()`. + * + * @see Item_Handler::get_item_handler_key_name() + */ + $cancel = apply_filters( + 'as3cf_pre_handle_item', + /** + * Filter fires before handling an action on an item, allows action to be cancelled. + * + * This is a handler specific filter whose name ends with the handler's key name. + * Format is `as3cf_pre_handle_item_{item-handler-key-name}`. + * + * Example filter names: + * + * as3cf_pre_handle_item_upload + * as3cf_pre_handle_item_download + * as3cf_pre_handle_item_remove-local + * as3cf_pre_handle_item_remove-provider + * as3cf_pre_handle_item_update-acl + * + * For a more generic filter, use `as3cf_pre_handle_item`. + * + * @param bool $cancel Should the action on the item be cancelled? + * @param Item $as3cf_item The item that the action is being handled for. + * @param array $options Handler dependent options that may have been set for the action. + * + * @see Item_Handler::get_item_handler_key_name() + */ + apply_filters( 'as3cf_pre_handle_item_' . static::get_item_handler_key_name(), false, $as3cf_item, $options ), + $as3cf_item, + $options, + static::get_item_handler_key_name() + ); + } catch ( Exception $e ) { + return $this->return_result( new WP_Error( $e->getMessage() ), $as3cf_item, $options ); + } + + // Cancelled, let caller know that request was not handled. + if ( false !== $cancel ) { + return $this->return_result( false, $as3cf_item, $options ); + } + + $manifest = $this->pre_handle( $as3cf_item, $options ); + if ( is_wp_error( $manifest ) ) { + return $this->return_result( $manifest, $as3cf_item, $options ); + } + + // Nothing to do, let caller know that request was not handled. + if ( empty( $manifest ) || empty( $manifest->objects ) ) { + return $this->return_result( false, $as3cf_item, $options ); + } + + $result = $this->handle_item( $as3cf_item, $manifest, $options ); + if ( is_wp_error( $result ) ) { + return $this->return_result( $result, $as3cf_item, $options ); + } + + $result = $this->post_handle( $as3cf_item, $manifest, $options ); + + return $this->return_result( $result, $as3cf_item, $options ); + } + + /** + * Process an Item and options to generate a Manifest for `handle_item`. + * + * @param Item $as3cf_item + * @param array $options + * + * @return Manifest|WP_Error + */ + abstract protected function pre_handle( Item $as3cf_item, array $options ); + + /** + * Perform action for Item using given Manifest. + * + * @param Item $as3cf_item + * @param Manifest $manifest + * @param array $options + * + * @return bool|WP_Error + */ + abstract protected function handle_item( Item $as3cf_item, Manifest $manifest, array $options ); + + /** + * Process results of `handle_item` as appropriate. + * + * @param Item $as3cf_item + * @param Manifest $manifest + * @param array $options + * + * @return bool|WP_Error + */ + abstract protected function post_handle( Item $as3cf_item, Manifest $manifest, array $options ); + + /** + * Helper to record errors and return meta data on handler error. + * + * @param string $error_msg + * @param array|null $return + * + * @return array|WP_Error + */ + protected function return_handler_error( $error_msg, $return = null ) { + AS3CF_Error::log( $error_msg ); + + if ( is_null( $return ) ) { + return new WP_Error( 'exception', $error_msg ); + } + + return $return; + } + + /** + * Fires a couple of actions to let interested parties know that a handler has returned a result. + * + * @param bool|WP_Error $result Result for the action, either handled (true/false), or an error. + * @param Item $as3cf_item The item that the action was being handled for. + * @param array $options Handler dependent options that may have been set for the action. + * + * @return bool|WP_Error + */ + private function return_result( $result, Item $as3cf_item, array $options ) { + /** + * Action fires after attempting to handle an action on an item. + * + * This is a handler specific action whose name ends with the handler's key name. + * Format is `as3cf_post_handle_item_{item-handler-key-name}`. + * + * Example filter names: + * + * as3cf_post_handle_item_upload + * as3cf_post_handle_item_download + * as3cf_post_handle_item_remove-local + * as3cf_post_handle_item_remove-provider + * as3cf_post_handle_item_update-acl + * + * For a more generic filter, use `as3cf_post_handle_item`. + * + * @param bool|WP_Error $result Result for the action, either handled (true/false), or an error. + * @param Item $as3cf_item The item that the action was being handled for. + * @param array $options Handler dependent options that may have been set for the action. + * + * @see Item_Handler::get_item_handler_key_name() + */ + do_action( 'as3cf_post_handle_item_' . static::get_item_handler_key_name(), $result, $as3cf_item, $options ); + + /** + * Action fires after attempting to handle an action on an item. + * + * This is a generic handler action that includes the handler's key name as the last param. + * + * @param bool|WP_Error $result Result for the action, either handled (true/false), or an error. + * @param Item $as3cf_item The item that the action was being handled for. + * @param array $options Handler dependent options that may have been set for the action. + * @param array $handler_key_name The handler's key name as per `Item_Handler::get_item_handler_key_name()`. + * + * @see Item_Handler::get_item_handler_key_name() + */ + do_action( 'as3cf_post_handle_item', $result, $as3cf_item, $options, static::get_item_handler_key_name() ); + + return $result; + } +} \ No newline at end of file diff --git a/classes/items/item.php b/classes/items/item.php index 9e90004b..29103329 100644 --- a/classes/items/item.php +++ b/classes/items/item.php @@ -3,21 +3,26 @@ namespace DeliciousBrains\WP_Offload_Media\Items; use Amazon_S3_And_CloudFront; +use AS3CF_Error; use AS3CF_Utils; +use DeliciousBrains\WP_Offload_Media\Providers\Storage\Storage_Provider; +use Exception; use WP_Error; abstract class Item { - const ITEMS_TABLE = 'as3cf_items'; - const ORIGINATORS = array( + const ITEMS_TABLE = 'as3cf_items'; + const ORIGINATORS = array( 'standard' => 0, 'metadata-tool' => 1, ); + const CAN_USE_OBJECT_VERSIONING = true; - protected static $source_type = 'media-library'; - protected static $source_table = 'posts'; - protected static $source_fk = 'id'; + protected static $source_type_name = 'Item'; + protected static $source_type = ''; + protected static $source_table = ''; + protected static $source_fk = ''; - private static $checked_table_exists = array(); + protected static $can_use_yearmonth = true; protected static $items_cache_by_id = array(); protected static $items_cache_by_source_id = array(); @@ -34,6 +39,8 @@ abstract class Item { 'source_path' => array( 'source_path', 'original_source_path' ), ); + private static $checked_table_exists = array(); + private $id; private $provider; private $region; @@ -51,18 +58,19 @@ abstract class Item { /** * Item constructor. * - * @param string $provider Storage provider key name, e.g. "aws". - * @param string $region Region for item's bucket. - * @param string $bucket Bucket for item. - * @param string $path Key path for item (full sized if type has thumbnails etc). - * @param bool $is_private Is the object private in the bucket. - * @param int $source_id ID that source has. - * @param string $source_path Path that source uses, could be relative or absolute depending on source. - * @param string $original_filename An optional filename with no path that was previously used for the item. - * @param array $extra_info An optional array of extra data specific to the source type. - * @param int $id Optional Item record ID. - * @param int $originator Optional originator of record from ORIGINATORS const. - * @param bool $is_verified Optional flag as to whether Item's objects are known to exist. + * @param string $provider Storage provider key name, e.g. "aws". + * @param string $region Region for item's bucket. + * @param string $bucket Bucket for item. + * @param string $path Key path for item (full sized if type has thumbnails etc). + * @param bool $is_private Is the object private in the bucket. + * @param int $source_id ID that source has. + * @param string $source_path Path that source uses, could be relative or absolute depending on source. + * @param string $original_filename An optional filename with no path that was previously used for the item. + * @param array $extra_info An optional array of extra data specific to the source type. + * @param int $id Optional Item record ID. + * @param int $originator Optional originator of record from ORIGINATORS const. + * @param bool $is_verified Optional flag as to whether Item's objects are known to exist. + * @param bool $use_object_versioning Optional flag as to whether path prefix should use Object Versioning if type allows it. */ public function __construct( $provider, @@ -76,34 +84,101 @@ public function __construct( $extra_info = array(), $id = null, $originator = 0, - $is_verified = true + $is_verified = true, + $use_object_versioning = self::CAN_USE_OBJECT_VERSIONING ) { + /** @var Amazon_S3_And_CloudFront $as3cf */ + global $as3cf; + + $this->source_id = $source_id; + $this->source_path = $source_path; + + if ( empty( $original_filename ) ) { + $this->original_source_path = $source_path; + } else { + $this->original_source_path = str_replace( wp_basename( $source_path ), $original_filename, $source_path ); + } + + // Set offload data from previous duplicate if exact match by source path exists. + if ( empty( $path ) ) { + $prev_items = static::get_by_source_path( array( $this->source_path, $this->original_source_path ), $this->source_id, true, true ); + + if ( ! is_wp_error( $prev_items ) && ! empty( $prev_items[0] ) && is_a( $prev_items[0], get_class( $this ) ) ) { + /** @var Item $prev_item */ + $prev_item = $prev_items[0]; + $provider = $prev_item->provider(); + $region = $prev_item->region(); + $bucket = $prev_item->bucket(); + $path = $prev_item->path(); + $is_private = $prev_item->is_private(); + $extra_info = $prev_item->extra_info(); + } + } + + // Not a duplicate, create a new path to offload to. + if ( empty( $path ) ) { + $prefix = $this->get_new_item_prefix( $use_object_versioning ); + $path = $prefix . wp_basename( $source_path ); + } + + if ( ! is_array( $extra_info ) ) { + $extra_info = array(); + } + + if ( ! isset( $extra_info['private_prefix'] ) || is_null( $extra_info['private_prefix'] ) ) { + $extra_info['private_prefix'] = ''; + if ( $as3cf->private_prefix_enabled() ) { + $extra_info['private_prefix'] = AS3CF_Utils::trailingslash_prefix( $as3cf->get_setting( 'signed-urls-object-prefix', '' ) ); + } + } + + if ( empty( $provider ) ) { + $provider = $as3cf->get_storage_provider()->get_provider_key_name(); + } + + if ( empty( $region ) ) { + $region = $as3cf->get_setting( 'region' ); + if ( is_wp_error( $region ) ) { + $region = ''; + } + } + + if ( empty( $bucket ) ) { + $bucket = $as3cf->get_setting( 'bucket' ); + } + $this->provider = $provider; $this->region = $region; $this->bucket = $bucket; $this->path = $path; - $this->is_private = $is_private; - $this->source_id = $source_id; - $this->source_path = $source_path; - $this->extra_info = serialize( $extra_info ); + $this->extra_info = $extra_info; $this->originator = $originator; $this->is_verified = $is_verified; if ( empty( $original_filename ) ) { - $this->original_path = $path; - $this->original_source_path = $source_path; + $this->original_path = $path; } else { - $this->original_path = str_replace( wp_basename( $path ), $original_filename, $path ); - $this->original_source_path = str_replace( wp_basename( $source_path ), $original_filename, $source_path ); + $this->original_path = str_replace( wp_basename( $path ), $original_filename, $path ); } if ( ! empty( $id ) ) { $this->id = $id; } + $this->set_is_private( (bool) $is_private ); + static::add_to_items_cache( $this ); } + /** + * Returns the standard object key for an items primary object + * + * @return string + */ + public static function primary_object_key() { + return '__as3cf_primary'; + } + /** * Returns the string used to group all keys in the object cache by. * @@ -115,6 +190,12 @@ protected static function get_object_cache_group() { if ( empty( $group ) ) { /** @var Amazon_S3_And_CloudFront $as3cf */ global $as3cf; + + /** + * Filters the object cache group name. + * + * @param string $group Defaults to 'as3cf' + */ $group = trim( '' . apply_filters( 'as3cf_object_cache_group', $as3cf->get_plugin_prefix() ) ); } @@ -463,7 +544,7 @@ public function key_values( $include_id = false ) { 'source_id' => $this->source_id, 'source_path' => $this->source_path, 'original_source_path' => $this->original_source_path, - 'extra_info' => $this->extra_info, + 'extra_info' => serialize( $this->extra_info ), 'originator' => $this->originator, 'is_verified' => $this->is_verified, ); @@ -477,28 +558,6 @@ public function key_values( $include_id = false ) { return $key_values; } - /** - * All the item's property names in an array, optionally with id if available. - * - * @param bool $include_id Default false. - * - * @return array - */ - private function keys( $include_id = false ) { - return array_keys( $this->key_values( $include_id ) ); - } - - /** - * All the item's property values in an array, optionally with id if available. - * - * @param bool $include_id Default false. - * - * @return array - */ - private function values( $include_id = false ) { - return array_values( $this->key_values( $include_id ) ); - } - /** * Get item's column formats as an associative array, optionally with id if available. * @@ -546,11 +605,15 @@ private function formats( $include_id = false ) { /** * Save the item's current data. * + * @param bool $update_duplicates If updating, also update records for duplicated source, defaults to true. + * * @return int|WP_Error */ - public function save() { + public function save( $update_duplicates = true ) { global $wpdb; + $update = false; + if ( empty( $this->id ) ) { $result = $wpdb->insert( static::items_table(), $this->key_values(), $this->formats() ); @@ -561,6 +624,8 @@ public function save() { static::add_to_items_cache( $this ); } } else { + $update = true; + // Make sure object cache does not have stale items. $old_item = static::get_from_object_cache( 'id', $this->id() ); static::remove_from_object_cache( $old_item ); @@ -578,6 +643,34 @@ public function save() { return new WP_Error( 'item_save', 'Error saving item:- ' . $wpdb->last_error ); } + // If one or more duplicate exists that still has the same source paths, keep them in step. + if ( $update && $update_duplicates ) { + $duplicates = static::get_by_source_path( array( $this->source_path, $this->original_source_path ), $this->source_id ); + + if ( ! empty( $duplicates ) && ! is_wp_error( $duplicates ) ) { + /** @var Item $duplicate */ + foreach ( $duplicates as $duplicate ) { + if ( + ! is_wp_error( $duplicate ) && + $duplicate->source_type() === $this->source_type() && + $duplicate->source_path() === $this->source_path() && + $duplicate->original_source_path() === $this->original_source_path() + ) { + $duplicate->provider = $this->provider; + $duplicate->region = $this->region; + $duplicate->bucket = $this->bucket; + $duplicate->path = $this->path; + $duplicate->original_path = $this->original_path; + $duplicate->is_private = $this->is_private; + $duplicate->extra_info = $this->extra_info; + $duplicate->originator = $this->originator; + $duplicate->is_verified = $this->is_verified; + $duplicate->save( false ); + } + } + } + } + return $this->id; } @@ -614,13 +707,29 @@ public function delete() { * @return Item */ protected static function create( $object, $add_to_object_cache = false ) { + /** @var Amazon_S3_And_CloudFront $as3cf */ + global $as3cf; + $extra_info = array(); if ( ! empty( $object->extra_info ) ) { $extra_info = unserialize( $object->extra_info ); + static::maybe_update_extra_info( $extra_info, $object->source_id, $object->is_private ); + } + + if ( ! empty( static::$source_type ) && static::$source_type !== $object->source_type ) { + AS3CF_Error::log( sprintf( 'Doing it wrong! Trying to create a %s class instance with data representing a %s', __CLASS__, $object->source_type ) ); + } + + if ( empty( static::$source_type ) ) { + /** @var Item $class */ + $class = $as3cf->get_source_type_class( $object->source_type ); + } else { + /** @var Item $class */ + $class = $as3cf->get_source_type_class( static::$source_type ); } - $item = new static( + $item = new $class( $object->provider, $object->region, $object->bucket, @@ -636,7 +745,7 @@ protected static function create( $object, $add_to_object_cache = false ) { ); if ( $add_to_object_cache ) { - static::add_to_object_cache( $item ); + $class::add_to_object_cache( $item ); } return $item; @@ -678,7 +787,7 @@ public static function get_by_id( $id ) { * * While source id isn't strictly unique, it is by source type, which is always used in queries based on called class. * - * @param integer $source_id + * @param int $source_id * * @return bool|Item */ @@ -697,7 +806,7 @@ public static function get_by_source_id( $source_id ) { $item = static::get_from_items_cache_by_source_id( $source_id ); - if ( ! empty( $item ) ) { + if ( ! empty( $item ) && ! empty( $item->id() ) ) { return $item; } @@ -712,6 +821,24 @@ public static function get_by_source_id( $source_id ) { return static::create( $object, true ); } + /** + * Getter for item's source type. + * + * @return string + */ + public static function source_type() { + return static::$source_type; + } + + /** + * Getter for item's source type name. + * + * @return string + */ + public static function source_type_name() { + return static::$source_type_name; + } + /** * Getter for item's id value. * @@ -739,6 +866,15 @@ public function region() { return $this->region; } + /** + * Setter for item's region value. + * + * @param string $region + */ + public function set_region( $region ) { + $this->region = $region; + } + /** * Getter for item's bucket value. * @@ -748,13 +884,45 @@ public function bucket() { return $this->bucket; } + /** + * Setter for item's bucket value. + * + * @param string $bucket + */ + public function set_bucket( $bucket ) { + $this->bucket = $bucket; + } + /** * Getter for item's path value. * + * The path is always the public representation, + * see provider_key() and provider_keys() for realised versions. + * + * @param string $object_key + * * @return string */ - public function path() { - return $this->path; + public function path( $object_key = null ) { + $path = $this->path; + + if ( ! empty( $object_key ) ) { + $objects = $this->objects(); + if ( isset( $objects[ $object_key ]['source_file'] ) ) { + $path = $this->prefix() . $objects[ $object_key ]['source_file']; + } + } + + return $path; + } + + /** + * Setter for item's path value. + * + * @param $path + */ + public function set_path( $path ) { + $this->path = $path; } /** @@ -766,22 +934,168 @@ public function original_path() { return $this->original_path; } + /** + * Setter for item's original path value. + * + * @param $path + */ + public function set_original_path( $path ) { + $this->original_path = $path; + } + /** * Getter for item's is_private value. * + * @param string|null $object_key + * * @return bool */ - public function is_private() { + public function is_private( $object_key = null ) { + if ( ! empty( $object_key ) ) { + $objects = $this->objects(); + if ( isset( $objects[ $object_key ]['is_private'] ) ) { + return (bool) $objects[ $object_key ]['is_private']; + } + + return false; + } + return (bool) $this->is_private; } /** * Setter for item's is_private value * - * @param bool $private + * @param bool $private + * @param string|null $object_key + */ + public function set_is_private( $private, $object_key = null ) { + if ( ! empty( $object_key ) ) { + $objects = $this->objects(); + if ( isset( $objects[ $object_key ] ) ) { + $objects[ $object_key ]['is_private'] = $private; + $this->set_objects( $objects ); + } + + if ( $object_key === Item::primary_object_key() ) { + $this->is_private = $private; + } + + return; + } + + $this->set_is_private( $private, Item::primary_object_key() ); + } + + /** + * Any private objects in this item + * + * @return bool */ - public function set_is_private( $private ) { - $this->is_private = (bool) $private; + public function has_private_objects() { + foreach ( $this->objects() as $object ) { + if ( $object['is_private'] ) { + return true; + } + } + + return false; + } + + /** + * Getter for the item prefix + * + * @return string + */ + public function prefix() { + $dirname = dirname( $this->path ); + $dirname = $dirname === '.' ? '' : $dirname; + + return AS3CF_Utils::trailingslash_prefix( $dirname ); + } + + /** + * Get the private prefix for item's private objects. + * + * @return string + */ + public function private_prefix() { + $extra_info = $this->extra_info(); + + if ( ! empty( $extra_info['private_prefix'] ) ) { + return AS3CF_Utils::trailingslash_prefix( $extra_info['private_prefix'] ); + } + + return ''; + } + + /** + * Setter for the private prefix + * + * @param string $new_private_prefix + */ + public function set_private_prefix( $new_private_prefix ) { + $extra_info = $this->extra_info(); + $extra_info['private_prefix'] = AS3CF_Utils::trailingslash_prefix( $new_private_prefix ); + $this->set_extra_info( $extra_info ); + } + + /** + * Get the full remote key for this item including private prefix when needed + * + * @param string|null $object_key + * + * @return string + */ + public function provider_key( $object_key = null ) { + $path = $this->path( $object_key ); + if ( $this->is_private( $object_key ) ) { + $path = $this->private_prefix() . $path; + } + + return $path; + } + + /** + * Returns an associative array of provider keys by their object_key. + * + * NOTE: There may be duplicate keys if object_keys reference same source file/object. + * + * @return array + */ + public function provider_keys() { + $keys = array(); + + foreach ( array_keys( $this->objects() ) as $object_key ) { + $keys[ $object_key ] = $this->provider_key( $object_key ); + } + + return $keys; + } + + /** + * Creates a provider key for a given filename using the item's prefix settings. + * + * This function can be used to create ad-hoc custom provider keys. + * There are no tests to see if the filename is known to be associated with the item. + * + * @param string $filename Just a filename without any path. + * @param bool $is_private Should a private prefixed provider key be created if appropriate? + * + * @return string + */ + public function provider_key_for_filename( $filename, $is_private ) { + $provider_key = ''; + + if ( ! empty( $filename ) ) { + $provider_key = $this->prefix() . wp_basename( trim( $filename ) ); + + if ( $is_private ) { + $provider_key = $this->private_prefix() . $provider_key; + } + } + + return $provider_key; } /** @@ -796,12 +1110,32 @@ public function source_id() { /** * Getter for item's source_path value. * + * @param string|null $object_key + * * @return string */ - public function source_path() { + public function source_path( $object_key = null ) { + if ( ! empty( $object_key ) ) { + $objects = $this->objects(); + if ( isset( $objects[ $object_key ] ) ) { + $object_file = $objects[ $object_key ]['source_file']; + + return str_replace( wp_basename( $this->source_path ), $object_file, $this->source_path ); + } + } + return $this->source_path; } + /** + * Setter for item's source_path value + * + * @param string $new_path + */ + public function set_source_path( $new_path ) { + $this->source_path = $new_path; + } + /** * Getter for item's original_source_path value. * @@ -811,22 +1145,80 @@ public function original_source_path() { return $this->original_source_path; } + /** + * Setter for item's original_source_path value + * + * @param string $new_path + */ + public function set_original_source_path( $new_path ) { + $this->original_source_path = $new_path; + } + + /** + * Get an absolute source path. + * + * Default it is based on the WordPress uploads folder. + * + * @param string|null $object_key Optional, by default the original file's source path is used. + * + * @return string + */ + public function full_source_path( $object_key = null ) { + /** + * Filter the absolute directory path prefix for an item's source files. + * + * @param string $basedir Default is WordPress uploads folder. + * @param Item $as3cf_item The Item whose full source path is being accessed. + */ + $basedir = trailingslashit( apply_filters( 'as3cf_item_basedir', wp_upload_dir()['basedir'], $this ) ); + + return $basedir . $this->source_path( $object_key ); + } + + /** + * Creates an absolute source path for a given filename using the item's source path settings. + * + * This function can be used to create ad-hoc custom source file paths. + * There are no tests to see if the filename is known to be associated with the item. + * + * Default it is based on the WordPress uploads folder. + * + * @param string $filename Just a filename without any path. + * + * @return string + */ + public function full_source_path_for_filename( $filename ) { + if ( empty( $filename ) ) { + return ''; + } + + /** + * Filter the absolute directory path prefix for an item's source files. + * + * @param string $basedir Default is WordPress uploads folder. + * @param Item $as3cf_item The Item whose full source path is being accessed. + */ + $basedir = trailingslashit( apply_filters( 'as3cf_item_basedir', wp_upload_dir()['basedir'], $this ) ); + + return $basedir . str_replace( wp_basename( $this->source_path ), wp_basename( trim( $filename ) ), $this->source_path ); + } + /** * Getter for item's extra_info value. * * @return array */ public function extra_info() { - return unserialize( $this->extra_info ); + return $this->extra_info; } /** - * Setter for extra_info value + * Setter for extra_info value. * * @param array $extra_info */ - protected function set_extra_info( $extra_info ) { - $this->extra_info = serialize( $extra_info ); + public function set_extra_info( $extra_info ) { + $this->extra_info = $extra_info; } /** @@ -838,6 +1230,15 @@ public function originator() { return $this->originator; } + /** + * Setter for item's originator value. + * + * @param int $originator + */ + public function set_originator( $originator ) { + $this->originator = $originator; + } + /** * Getter for item's is_verified value. * @@ -848,7 +1249,7 @@ public function is_verified() { } /** - * Setter for item's is_verified value + * Setter for item's is_verified value. * * @param bool $is_verified */ @@ -856,6 +1257,15 @@ public function set_is_verified( $is_verified ) { $this->is_verified = (bool) $is_verified; } + /** + * Does this item type use object versioning? + * + * @return bool + */ + public static function can_use_object_versioning() { + return static::CAN_USE_OBJECT_VERSIONING; + } + /** * Get normalized object path dir. * @@ -912,14 +1322,12 @@ public static function get_source_id_by_bucket_and_path( $bucket, $path ) { * * @param string $url * - * @return int|bool + * @return array|bool */ - public static function get_source_id_by_remote_url( $url ) { + public static function get_item_source_by_remote_url( $url ) { global $wpdb; - /** - * @var Amazon_S3_And_CloudFront|\Amazon_S3_And_CloudFront_Pro $as3cf - */ + /** @var Amazon_S3_And_CloudFront $as3cf */ global $as3cf; $parts = AS3CF_Utils::parse_url( $url ); @@ -952,8 +1360,7 @@ public static function get_source_id_by_remote_url( $url ) { } $sql = $wpdb->prepare( - "SELECT * FROM " . static::items_table() . " WHERE source_type = %s AND (path LIKE %s OR original_path LIKE %s);" - , static::$source_type + "SELECT * FROM " . static::items_table() . " WHERE (path LIKE %s OR original_path LIKE %s);" , '%' . $path , '%' . $path ); @@ -970,7 +1377,9 @@ public static function get_source_id_by_remote_url( $url ) { $path = AS3CF_Utils::decode_filename_in_path( ltrim( $parts['path'], '/' ) ); foreach ( $results as $result ) { - $as3cf_item = static::create( $result ); + /** @var Item $class */ + $class = $as3cf->get_source_type_class( $result->source_type ); + $as3cf_item = $class::create( $result ); // If item's bucket matches first segment of URL path, remove it from URL path before checking match. if ( 0 === strpos( $path, trailingslashit( $as3cf_item->bucket() ) ) ) { @@ -986,7 +1395,10 @@ public static function get_source_id_by_remote_url( $url ) { // Exact match, return ID. if ( $as3cf_item->path() === $match_path || $as3cf_item->original_path() === $match_path ) { - return $as3cf_item->source_id(); + return array( + 'id' => $as3cf_item->source_id(), + 'source_type' => $as3cf_item->source_type(), + ); } } @@ -998,11 +1410,11 @@ public static function get_source_id_by_remote_url( $url ) { * * While source id isn't strictly unique, it is by source type, which is always used in queries based on called class. * - * @param integer $upper_bound Returned source_ids should be lower than this, use null/0 for no upper bound. - * @param integer $limit Maximum number of source_ids to return. Required if not counting. - * @param bool $count Just return a count of matching source_ids? Negates $limit, default false. - * @param int $originator Optionally restrict to only records with given originator type from ORIGINATORS const. - * @param bool $is_verified Optionally restrict to only records that either are or are not verified. + * @param int $upper_bound Returned source_ids should be lower than this, use null/0 for no upper bound. + * @param int $limit Maximum number of source_ids to return. Required if not counting. + * @param bool $count Just return a count of matching source_ids? Negates $limit, default false. + * @param int $originator Optionally restrict to only records with given originator type from ORIGINATORS const. + * @param bool $is_verified Optionally restrict to only records that either are or are not verified. * * @return array|int */ @@ -1030,7 +1442,7 @@ public static function get_source_ids( $upper_bound, $limit, $count = false, $or $sql .= ' AND originator = %d'; $args[] = $originator; } else { - \AS3CF_Error::log( __METHOD__ . ' called with invalid originator: ' . $originator ); + AS3CF_Error::log( __METHOD__ . ' called with invalid originator: ' . $originator ); return $count ? 0 : array(); } @@ -1042,7 +1454,7 @@ public static function get_source_ids( $upper_bound, $limit, $count = false, $or $sql .= ' AND is_verified = %d'; $args[] = (int) $is_verified; } else { - \AS3CF_Error::log( __METHOD__ . ' called with invalid is_verified: ' . $is_verified ); + AS3CF_Error::log( __METHOD__ . ' called with invalid is_verified: ' . $is_verified ); return $count ? 0 : array(); } @@ -1067,9 +1479,9 @@ public static function get_source_ids( $upper_bound, $limit, $count = false, $or * * While source id isn't strictly unique, it is by source type, which is always used in queries based on called class. * - * @param integer $upper_bound Returned source_ids should be lower than this, use null/0 for no upper bound. - * @param integer $limit Maximum number of source_ids to return. Required if not counting. - * @param bool $count Just return a count of matching source_ids? Negates $limit, default false. + * @param int $upper_bound Returned source_ids should be lower than this, use null/0 for no upper bound. + * @param int $limit Maximum number of source_ids to return. Required if not counting. + * @param bool $count Just return a count of matching source_ids? Negates $limit, default false. * * @return array|int * @@ -1084,11 +1496,475 @@ public static function get_missing_source_ids( $upper_bound, $limit, $count = fa } /** - * Get absolute file paths associated with source item. + * Get array of objects (i.e. different sizes of same attachment item) * - * @param integer $id + * @return array + */ + public function objects() { + $extra_info = $this->extra_info(); + if ( isset( $extra_info['objects'] ) && is_array( $extra_info['objects'] ) ) { + // Make sure that the primary object key, if exists, comes first + $array_keys = array_keys( $extra_info['objects'] ); + $primary_key = Item::primary_object_key(); + if ( in_array( $primary_key, $array_keys ) && $primary_key !== $array_keys[0] ) { + $extra_info['objects'] = array_merge( array( $primary_key => null ), $extra_info['objects'] ); + } + + return $extra_info['objects']; + } + + return array(); + } + + /** + * Set array of objects (i.e. different sizes of same attachment item) + * + * @param array $objects + */ + public function set_objects( $objects ) { + $extra_info = $this->extra_info(); + + $extra_info['objects'] = $objects; + $this->set_extra_info( $extra_info ); + } + + /** + * Synthesize a data struct to be used when passing information + * about the current item to filters that assume the item is a + * media library item. * * @return array */ - abstract protected function source_paths( $id ); + public function item_data_for_acl_filter() { + return array( + 'source_type' => $this->source_type(), + 'file' => $this->path( Item::primary_object_key() ), + 'sizes' => array_keys( $this->objects() ), + ); + } + + /** + * Get absolute source file paths for offloaded files. + * + * @return array Associative array of object_key => path + */ + abstract public function full_source_paths(); + + /** + * Get size name from file name. + * + * @return string + */ + abstract public function get_object_key_from_filename( $filename ); + + /** + * Get the provider URL for an item + * + * @param string|null $object_key + * + * @return string|false + */ + public abstract function get_local_url( $object_key = null ); + + /** + * Create a new item from the source id. + * + * @param int $source_id + * @param array $options + * + * @return Item|WP_Error + */ + public static function create_from_source_id( $source_id, $options = array() ) { + return new WP_Error( + 'exception', + sprintf( 'Doing it wrong! Trying to create a base %s class instance from source ID %d', __CLASS__, $source_id ) + ); + } + + /** + * Return a year/month string for the item + * + * @return string + */ + protected function get_item_time() { + return null; + } + + /** + * Return an additional 'internal' prefix used by some item types + * + * @return string + */ + protected function get_internal_prefix() { + return ''; + } + + /** + * Get item's new public prefix path for current settings. + * + * @param bool $use_object_versioning + * + * @return string + */ + public function get_new_item_prefix( $use_object_versioning = true ) { + /** @var Amazon_S3_And_CloudFront $as3cf */ + global $as3cf; + + $prefix = $as3cf->get_object_prefix(); + + $time = $this->get_item_time(); + $prefix .= AS3CF_Utils::trailingslash_prefix( $as3cf->get_dynamic_prefix( $time, static::$can_use_yearmonth ) ); + + if ( $use_object_versioning && static::can_use_object_versioning() && $as3cf->get_setting( 'object-versioning' ) ) { + $prefix .= AS3CF_Utils::trailingslash_prefix( $as3cf->get_object_version_string() ); + } + + return AS3CF_Utils::trailingslash_prefix( $prefix ); + } + + /** + * Get ACL for object key + * + * @param string $object_key Object key + * @param string|null $bucket Optional bucket that ACL is potentially to be used with. + * + * @return string|null + */ + public function get_acl_for_object_key( $object_key, $bucket = null ) { + /** @var Amazon_S3_And_CloudFront $as3cf */ + global $as3cf; + + $acl = null; + $use_acl = $as3cf->use_acl_for_intermediate_size( 0, $object_key, $bucket, $this ); + + if ( $use_acl ) { + $acl = $this->is_private( $object_key ) ? $as3cf->get_storage_provider()->get_private_acl() : $as3cf->get_storage_provider()->get_default_acl(); + } + + return $acl; + } + + /** + * Search for all items that have the source path(s). + * + * @param array|string $paths Array of relative source paths. + * @param array|int $exclude_source_ids Array of source_ids to exclude from search. Default, none. + * @param bool $exact_match Use paths as supplied (true, default), or greedy match on path without extension (e.g. find edited too). + * @param bool $first_only Only return first matched item sorted by source_id. Default false. + * + * @return array + */ + public static function get_by_source_path( $paths, $exclude_source_ids = array(), $exact_match = true, $first_only = false ) { + global $wpdb; + + if ( ! is_array( $paths ) && is_string( $paths ) && ! empty( $paths ) ) { + $paths = array( $paths ); + } + + if ( ! is_array( $paths ) || empty( $paths ) ) { + return array(); + } + + $paths = AS3CF_Utils::make_upload_file_paths_relative( array_unique( $paths ) ); + + $sql = ' + SELECT DISTINCT items.* + FROM ' . static::items_table() . ' AS items USE INDEX (uidx_source_path, uidx_original_source_path) + WHERE 1=1 + '; + + if ( ! empty( $exclude_source_ids ) ) { + if ( ! is_array( $exclude_source_ids ) ) { + $exclude_source_ids = array( $exclude_source_ids ); + } + + $sql .= ' AND items.source_id NOT IN (' . join( ',', $exclude_source_ids ) . ')'; + } + + if ( $exact_match ) { + $sql .= " AND (items.source_path IN ('" . join( "','", $paths ) . "')"; + $sql .= " OR items.original_source_path IN ('" . join( "','", $paths ) . "'))"; + } else { + $likes = array_map( function ( $path ) { + $ext = '.' . pathinfo( $path, PATHINFO_EXTENSION ); + $path = substr_replace( $path, '%', -strlen( $ext ) ); + + return "items.source_path LIKE '" . $path . "' OR items.original_source_path LIKE '" . $path . "'"; + }, $paths ); + + $sql .= ' AND (' . join( ' OR ', $likes ) . ')'; + } + + if ( $first_only ) { + $sql .= ' ORDER BY items.source_id LIMIT 1'; + } + + return array_map( 'static::create', $wpdb->get_results( $sql ) ); + } + + /** + * Update path and original path with a new prefix + * + * @param string $new_prefix + */ + public function update_path_prefix( $new_prefix ) { + $this->set_path( $new_prefix . wp_basename( $this->path() ) ); + $this->set_original_path( $new_prefix . wp_basename( $this->original_path() ) ); + } + + /** + * Returns a link to the items edit page in WordPress + * + * @param object $error + * + * @return object|null Null or object containing properties 'url' and 'text' + */ + public static function admin_link( $error ) { + return null; + } + + /** + * Is the item served by provider. + * + * @param bool $skip_rewrite_check Still check if offloaded even if not currently rewriting URLs? Default: false + * @param bool $skip_current_provider_check Skip checking if offloaded to current provider. Default: false, negated if $provider supplied + * @param Storage_Provider|null $provider Provider where item is expected to be offloaded to. Default: currently configured provider + * @param bool $check_is_verified Check that metadata is verified, has no effect if $skip_rewrite_check is true. Default: false + * + * @return bool + */ + public function served_by_provider( $skip_rewrite_check = false, $skip_current_provider_check = false, Storage_Provider $provider = null, $check_is_verified = false ) { + /** @var Amazon_S3_And_CloudFront $as3cf */ + global $as3cf; + + if ( ! $skip_rewrite_check && ! $as3cf->get_setting( 'serve-from-s3' ) ) { + // Not serving provider URLs + return false; + } + + if ( ! $skip_rewrite_check && ! empty( $check_is_verified ) && ! $this->is_verified() ) { + // Offload not verified, treat as not offloaded. + return false; + } + + if ( ! $skip_current_provider_check && empty( $provider ) ) { + $provider = $as3cf->get_storage_provider(); + } + + if ( ! empty( $provider ) && $provider::get_provider_key_name() !== $this->provider() ) { + // File not uploaded to required provider + return false; + } + + return true; + } + + /** + * Does the item's files exist locally? + * + * @return bool + */ + public function exists_locally() { + foreach ( $this->full_source_paths() as $path ) { + if ( file_exists( $path ) ) { + return true; + } + } + + return false; + } + + /** + * Get the provider URL for an item + * + * @param string $object_key + * @param null|int $expires + * @param array $headers + * + * @return string|WP_Error|bool + */ + public function get_provider_url( $object_key = null, $expires = null, $headers = array() ) { + /** @var Amazon_S3_And_CloudFront $as3cf */ + global $as3cf; + + if ( is_null( $object_key ) ) { + $object_key = Item::primary_object_key(); + } + + // Is a signed expiring URL required for the requested object? + if ( is_null( $expires ) ) { + $expires = $this->is_private( $object_key ) ? Amazon_S3_And_CloudFront::DEFAULT_EXPIRES : null; + } else { + $expires = $this->is_private( $object_key ) ? $expires : null; + } + + $scheme = $as3cf->get_url_scheme(); + $enable_delivery_domain = $as3cf->get_delivery_provider()->delivery_domain_allowed() ? $as3cf->get_setting( 'enable-delivery-domain' ) : false; + $delivery_domain = $as3cf->get_setting( 'delivery-domain' ); + $item_path = $this->path( $object_key ); + + if ( ! $enable_delivery_domain || empty( $delivery_domain ) ) { + $region = $this->region(); + + if ( is_wp_error( $region ) ) { + return $region; + } + + $delivery_domain = $as3cf->get_storage_provider()->get_url_domain( $this->bucket(), $region, $expires ); + } else { + $delivery_domain = AS3CF_Utils::sanitize_custom_domain( $delivery_domain ); + } + + if ( ! is_null( $expires ) && $as3cf->is_plugin_setup( true ) ) { + try { + /** + * Filters the expires time for private content + * + * @param int $expires The expires time in seconds + */ + $timestamp = time() + apply_filters( 'as3cf_expires', $expires ); + $url = $as3cf->get_delivery_provider()->get_signed_url( $this, $item_path, $delivery_domain, $scheme, $timestamp, $headers ); + + /** + * Filters the secure URL for private content + * + * @param string $url The URL + * @param Item $item The Item object + * @param array $item_source The item source descriptor array + * @param int $timestamp Expiry timestamp + * @param array $headers Optional extra http headers + */ + return apply_filters( 'as3cf_get_item_secure_url', $url, $this, $this->get_item_source_array(), $timestamp, $headers ); + } catch ( Exception $e ) { + return new WP_Error( 'exception', $e->getMessage() ); + } + } else { + try { + $url = $as3cf->get_delivery_provider()->get_url( $this, $item_path, $delivery_domain, $scheme, $headers ); + + /** + * Filters the URL for public content + * + * @param string $url The URL + * @param Item $item The Item object + * @param array $item_source The item source descriptor array + * @param int $source_id The source ID of the object + * @param int $timestamp Expiry timestamp + * @param array $headers Optional extra http headers + */ + return apply_filters( 'as3cf_get_item_url', $url, $this, $this->get_item_source_array(), $expires, $headers ); + } catch ( Exception $e ) { + return new WP_Error( 'exception', $e->getMessage() ); + } + } + } + + /** + * Update file sizes after removing local files for an item + * + * @param int $original_size + * @param int $total_size + */ + public function update_filesize_after_remove_local( $original_size, $total_size ) { + } + + /** + * Cleanup file sizes after getting item files back from the bucket + */ + public function update_filesize_after_download_local() { + } + + /** + * If another item in current site shares full size *local* paths, only remove remote files not referenced by duplicates. + * We reference local paths as they should be reflected one way or another remotely, including backups. + * + * @params Item $as3cf_item + * @params array $paths + */ + public function remove_duplicate_paths( Item $as3cf_item, $paths ) { + return $paths; + } + + /** + * Verify that the extra info uses the new format set in plugin version 2.6.0 + * Update if needed + * + * @param array $extra_info + * @param int $source_id + * @param bool $is_private + * + * @since 2.6.0 + */ + protected static function maybe_update_extra_info( &$extra_info, $source_id, $is_private ) { + if ( ! is_array( $extra_info ) ) { + return; + } + + // Compatibility fallback for if just an array of private sizes is supplied. + $private_sizes = array(); + if ( ! isset( $extra_info['private_sizes'] ) && ! isset( $extra_info['private_prefix'] ) && ! isset( $extra_info['objects'] ) ) { + $private_sizes = $extra_info; + } + + if ( ! isset( $extra_info['objects'] ) ) { + $private_sizes = isset( $extra_info['private_sizes'] ) && is_array( $extra_info['private_sizes'] ) ? $extra_info['private_sizes'] : $private_sizes; + $extra_info['objects'] = array(); + + $files = AS3CF_Utils::get_attachment_file_paths( $source_id, false ); + foreach ( $files as $object_key => $file ) { + if ( 'file' === $object_key ) { + continue; + } + + $new_object = array( + 'source_file' => wp_basename( $file ), + 'is_private' => Item::primary_object_key() === $object_key ? $is_private : in_array( $object_key, $private_sizes ), + ); + + $extra_info['objects'][ $object_key ] = $new_object; + } + } + + if ( isset( $extra_info['private_sizes'] ) ) { + unset( $extra_info['private_sizes'] ); + } + } + + /** + * Returns the item source description array for this item + * + * @return array Array with the format: + * array( + * 'id' => 1, + * 'source_type' => 'foo-type', + * ) + */ + public function get_item_source_array() { + return array( + 'id' => $this->source_id(), + 'source_type' => $this->source_type(), + ); + } + + /** + * Returns an array keyed by offloaded source file name. + * + * Each entry is as per objects, but also includes an array of object_keys. + * + * @return array + */ + public function offloaded_files() { + $offloaded_files = array(); + + foreach ( $this->objects() as $object_key => $object ) { + if ( isset( $offloaded_files[ $object['source_file'] ] ) ) { + $offloaded_files[ $object['source_file'] ]['object_keys'][] = $object_key; + } else { + $object['object_keys'] = array( $object_key ); + $offloaded_files[ $object['source_file'] ] = $object; + } + } + + return $offloaded_files; + } } \ No newline at end of file diff --git a/classes/items/manifest.php b/classes/items/manifest.php new file mode 100644 index 00000000..8f2c5279 --- /dev/null +++ b/classes/items/manifest.php @@ -0,0 +1,10 @@ + ['thumbnail', 'medium', ...] - * 'private_prefix' => 'private/' - * For backwards compatibility, if a simple array is supplied it is treated as - * private thumbnail sizes that should be private objects in the bucket. - * @param int $id Optional Item record ID. - * @param int $originator Optional originator of record from ORIGINATORS const. - * @param bool $is_verified Optional flag as to whether Item's objects are known to exist. + * @param string $provider Storage provider key name, e.g. "aws". + * @param string $region Region for item's bucket. + * @param string $bucket Bucket for item. + * @param string $path Key path for item (full sized if type has thumbnails etc). + * @param bool $is_private Is the object private in the bucket. + * @param int $source_id ID that source has. + * @param string $source_path Path that source uses, could be relative or absolute depending on source. + * @param string $original_filename An optional filename with no path that was previously used for the item. + * @param array $extra_info An optional associative array of extra data to be associated with the item. + * Recognised keys: + * 'objects' => array of ... + * -- 'thumbnail' => array of ... + * -- -- 'source_file' => 'image-150x150.png' + * -- -- 'is_private' => false + * 'private_prefix' => 'private/' + * For backwards compatibility, if a simple array is supplied it is treated as + * private thumbnail sizes that should be private objects in the bucket. + * @param int $id Optional Item record ID. + * @param int $originator Optional originator of record from ORIGINATORS const. + * @param bool $is_verified Optional flag as to whether Item's objects are known to exist. + * @param bool $use_object_versioning Optional flag as to whether path prefix should use Object Versioning if type allows it. */ public function __construct( $provider, @@ -43,7 +75,8 @@ public function __construct( $extra_info = array(), $id = null, $originator = 0, - $is_verified = true + $is_verified = true, + $use_object_versioning = self::CAN_USE_OBJECT_VERSIONING ) { // For Media Library items, the source path should be relative to the Media Library's uploads directory. $uploads = wp_upload_dir(); @@ -52,155 +85,201 @@ public function __construct( $source_path = AS3CF_Utils::unleadingslashit( substr( $source_path, strlen( $uploads['basedir'] ) ) ); } - $private_sizes = array(); - $private_prefix = ''; + $objects = array(); + $private_prefix = null; // Ensure re-hydration is clean. if ( ! empty( $extra_info ) && is_array( $extra_info ) ) { - if ( isset( $extra_info['private_sizes'] ) ) { - $private_sizes = $extra_info['private_sizes']; - } if ( isset( $extra_info['private_prefix'] ) ) { $private_prefix = $extra_info['private_prefix']; } - - // Compatibility fallback for if just an array of private sizes is supplied. - if ( ! isset( $extra_info['private_sizes'] ) && ! isset( $extra_info['private_prefix'] ) ) { - $private_sizes = $extra_info; + if ( isset( $extra_info['objects'] ) ) { + $objects = $extra_info['objects']; } } $extra_info = array( - 'private_sizes' => $private_sizes, + 'objects' => $objects, 'private_prefix' => $private_prefix, ); - parent::__construct( $provider, $region, $bucket, $path, $is_private, $source_id, $source_path, $original_filename, $extra_info, $id, $originator, $is_verified ); + parent::__construct( $provider, $region, $bucket, $path, $is_private, $source_id, $source_path, $original_filename, $extra_info, $id, $originator, $is_verified, $use_object_versioning ); } /** - * Get a new Media_Library_Item with all data derived from attachment data and current settings. - * - * @param int $attachment_id Attachment ID to construct record from. - * @param bool $object_versioning_allowed Can an Object Versioning string be appended if setting turned on? Default true. - * @param int $originator Originator of new record. Optional, default standard (0). + * Synthesize a data struct to be used when passing information + * about the current item to filters that assume the item is a + * media library item. * - * @return Media_Library_Item|WP_Error + * @return array */ - public static function create_from_attachment( $attachment_id, $object_versioning_allowed = true, $originator = 0 ) { - /** @var Amazon_S3_And_CloudFront $as3cf */ - global $as3cf; + public function item_data_for_acl_filter() { + $item_data = parent::item_data_for_acl_filter(); + $media_library_item_data = wp_get_attachment_metadata( $this->source_id(), true ); + + // Copy over specific elements only as i.e. 'size' may not be populated yet in $media_library_item_data + foreach ( array( 'file', 'original_image', 'image_meta' ) as $element ) { + if ( isset( $media_library_item_data[ $element ] ) ) { + $item_data[ $element ] = $media_library_item_data[ $element ]; + } + } - if ( empty( $attachment_id ) ) { + return $item_data; + } + + /** + * Create a new item from the source id. + * + * @param int $source_id + * @param array $options + * + * @return Item|WP_Error + */ + public static function create_from_source_id( $source_id, $options = array() ) { + if ( empty( $source_id ) ) { return new WP_Error( 'exception', __( 'Empty Attachment ID passed to ' . __FUNCTION__, 'amazon-s3-and-cloudfront' ) ); } - $object_versioning_allowed = empty( $object_versioning_allowed ) ? false : true; + $default_options = array( + 'originator' => Item::ORIGINATORS['standard'], + 'is_verified' => true, + 'use_object_versioning' => static::can_use_object_versioning(), + ); + + $options = array_merge( $default_options, $options ); - if ( ! in_array( $originator, self::ORIGINATORS ) ) { + if ( ! in_array( $options['originator'], self::ORIGINATORS ) ) { return new WP_Error( 'exception', __( 'Invalid Originator passed to ' . __FUNCTION__, 'amazon-s3-and-cloudfront' ) ); } - // If we ever expand originators to include more pre-verified versions, this will need changing. - $is_verified = 0 === $originator; - /* - * Provider basics. - */ - - $provider = $as3cf->get_storage_provider()->get_provider_key_name(); - $region = $as3cf->get_setting( 'region' ); - if ( is_wp_error( $region ) ) { - $region = ''; - } - $bucket = $as3cf->get_setting( 'bucket' ); - - /* - * Derive local and remote paths. + * Derive local path. */ // Verify that get_attached_file will not blow up as it does not check the data it manipulates. - $attached_file_meta = get_post_meta( $attachment_id, '_wp_attached_file', true ); + $attached_file_meta = get_post_meta( $source_id, '_wp_attached_file', true ); if ( ! is_string( $attached_file_meta ) ) { return new WP_Error( 'exception', - sprintf( __( 'Media Library item with ID %d has damaged meta data', 'amazon-s3-and-cloudfront' ), $attachment_id ) + sprintf( __( 'Media Library item with ID %d has damaged meta data', 'amazon-s3-and-cloudfront' ), $source_id ) ); } unset( $attached_file_meta ); - $source_path = get_attached_file( $attachment_id, true ); + $source_path = get_attached_file( $source_id, true ); // Check for valid "full" file path otherwise we'll not be able to create offload path or download in the future. if ( empty( $source_path ) ) { return new WP_Error( 'exception', - sprintf( __( 'Media Library item with ID %d does not have a valid file path', 'amazon-s3-and-cloudfront' ), $attachment_id ) + sprintf( __( 'Media Library item with ID %d does not have a valid file path', 'amazon-s3-and-cloudfront' ), $source_id ) ); } - $attachment_metadata = wp_get_attachment_metadata( $attachment_id, true ); - + /** @var array|false|WP_Error $attachment_metadata */ + $attachment_metadata = wp_get_attachment_metadata( $source_id, true ); if ( is_wp_error( $attachment_metadata ) ) { return $attachment_metadata; } - $prefix = $as3cf->get_new_attachment_prefix( $attachment_id, $attachment_metadata, $object_versioning_allowed ); - $path = $prefix . wp_basename( $source_path ); - - // There may be an original image that can override the default original filename. - $original_filename = empty( $attachment_metadata['original_image'] ) ? null : $attachment_metadata['original_image']; - - /* - * Private file handling. - */ - - $acl = apply_filters( 'as3cf_upload_acl', $as3cf->get_storage_provider()->get_default_acl(), $attachment_metadata, $attachment_id ); - $is_private = ! empty( $acl ) && $as3cf->get_storage_provider()->get_private_acl() === $acl; - - // Maybe set private sizes and private prefix. + // Initialize extra info array with empty values $extra_info = array( - 'private_sizes' => array(), - 'private_prefix' => '', + 'private_prefix' => null, + 'objects' => array(), ); - $file_paths = AS3CF_Utils::get_attachment_file_paths( $attachment_id, false, $attachment_metadata ); - $file_paths = array_diff( $file_paths, array( $source_path ) ); + // There may be an original image that can override the default original filename. + $original_filename = empty( $attachment_metadata['original_image'] ) ? null : $attachment_metadata['original_image']; + $file_paths = AS3CF_Utils::get_attachment_file_paths( $source_id, false, $attachment_metadata ); foreach ( $file_paths as $size => $size_file_path ) { - $acl = apply_filters( 'as3cf_upload_acl_sizes', $as3cf->get_storage_provider()->get_default_acl(), $size, $attachment_id, $attachment_metadata ); - - if ( ! empty( $acl ) && $as3cf->get_storage_provider()->get_private_acl() === $acl ) { - $extra_info['private_sizes'][] = $size; + if ( $size === 'file' ) { + continue; } - } - if ( $as3cf->private_prefix_enabled() ) { - $extra_info['private_prefix'] = AS3CF_Utils::trailingslash_prefix( $as3cf->get_setting( 'signed-urls-object-prefix', '' ) ); + $new_object = array( + 'source_file' => wp_basename( $size_file_path ), + 'is_private' => false, + ); + + $extra_info['objects'][ $size ] = $new_object; } return new self( - $provider, - $region, - $bucket, - $path, - $is_private, - $attachment_id, + '', + '', + '', + '', + false, + $source_id, $source_path, $original_filename, $extra_info, null, - $originator, - $is_verified + $options['originator'], + $options['is_verified'], + $options['use_object_versioning'] ); } + /** + * Get attachment local URL. + * + * This is partly a direct copy of wp_get_attachment_url() from /wp-includes/post.php + * as we filter the URL in AS3CF and can't remove this filter using the current implementation + * of globals for class instances. + * + * @param string|null $object_key + * + * @return string|false + */ + public function get_local_url( $object_key = null ) { + /** @var Amazon_S3_And_CloudFront $as3cf */ + global $as3cf; + $url = ''; + + // Get attached file. + if ( $file = get_post_meta( $this->source_id(), '_wp_attached_file', true ) ) { + // Get upload directory. + if ( ( $uploads = wp_upload_dir() ) && false === $uploads['error'] ) { + // Check that the upload base exists in the file location. + if ( 0 === strpos( $file, $uploads['basedir'] ) ) { + // Replace file location with url location. + $url = str_replace( $uploads['basedir'], $uploads['baseurl'], $file ); + } elseif ( false !== strpos( $file, 'wp-content/uploads' ) ) { + $url = $uploads['baseurl'] . substr( $file, strpos( $file, 'wp-content/uploads' ) + 18 ); + } else { + // It's a newly-uploaded file, therefore $file is relative to the basedir. + $url = $uploads['baseurl'] . "/$file"; + } + } + } + + if ( empty( $url ) ) { + return false; + } + + $url = $as3cf->maybe_fix_local_subsite_url( $url ); + + if ( ! empty( $object_key ) ) { + $meta = get_post_meta( $this->source_id(), '_wp_attachment_metadata', true ); + if ( empty( $meta['sizes'][ $object_key ]['file'] ) ) { + // No alternative sizes available, return + return $url; + } + + $url = str_replace( wp_basename( $url ), $meta['sizes'][ $object_key ]['file'], $url ); + } + + return $url; + } + /** * (Re)initialize the static cache used for speeding up queries. */ @@ -214,7 +293,7 @@ public static function init_cache() { /** * Get the item based on source id. * - * @param integer $source_id + * @param int $source_id * * @return bool|Media_Library_Item */ @@ -254,10 +333,10 @@ public function key( $filename = null ) { } if ( ! empty( $this->private_prefix() ) ) { - $size = \AS3CF_Utils::get_intermediate_size_from_filename( $this->source_id(), $filename ); + $size = $this->get_object_key_from_filename( $filename ); // Private path. - if ( $this->is_private_size( $size ) ) { + if ( $this->is_private( $size ) ) { return $this->private_prefix() . $this->normalized_path_dir() . $filename; } } @@ -267,116 +346,36 @@ public function key( $filename = null ) { } /** - * Get absolute file paths associated with source item. - * - * @param integer $id + * Get absolute source file paths for offloaded files. * - * @return array + * @return array Associative array of object_key => path */ - protected function source_paths( $id ) { - $paths = array(); - - return $paths; + public function full_source_paths() { + return array_intersect_key( AS3CF_Utils::get_attachment_file_paths( $this->source_id(), false ), $this->objects() ); } /** - * Getter for item's path value, optionally for a specific size - * - * @param null|string $size + * Get size name from file name * * @return string */ - public function path( $size = null ) { - $path = parent::path(); - - if ( empty( $size ) ) { - return $path; - } - - $meta = get_post_meta( $this->source_id(), '_wp_attachment_metadata', true ); - if ( ! empty( $meta['sizes'][ $size ]['file'] ) ) { - $path = str_replace( wp_basename( $path ), $meta['sizes'][ $size ]['file'], $path ); - } - - return $path; - } - - /** - * Get the array of thumbnail sizes that are private in the bucket. - * - * @return array - */ - public function private_sizes() { - $extra_info = $this->extra_info(); - - if ( ! empty( $extra_info['private_sizes'] ) ) { - // There was an issue with class re-hydration that meant empty private sizes embedded itself inside its key. - if ( - isset( $extra_info['private_sizes']['private_sizes'] ) && - is_array( $extra_info['private_sizes']['private_sizes'] ) && - empty( $extra_info['private_sizes']['private_sizes'] ) - ) { - unset( $extra_info['private_sizes']['private_sizes'] ); - } - - return $extra_info['private_sizes']; - } - - return array(); + public function get_object_key_from_filename( $filename ) { + return AS3CF_Utils::get_intermediate_size_from_filename( $this->source_id(), basename( $filename ) ); } /** - * Set the private status for a specific size. + * Get ACL for intermediate size. * - * @param $size - * @param $private - */ - public function set_private_size( $size, $private ) { - if ( empty( $size ) || AS3CF_Utils::is_full_size( $size ) ) { - return; - } - - $extra_info = $this->extra_info(); - $private_sizes = $this->private_sizes(); - if ( $private && ! in_array( $size, $private_sizes, true ) ) { - $private_sizes[] = $size; - } - if ( ! $private && in_array( $size, $private_sizes, true ) ) { - $private_sizes = array_diff( $private_sizes, array( $size ) ); - } - $extra_info['private_sizes'] = $private_sizes; - - $this->set_extra_info( $extra_info ); - } - - /** - * Get the private status for a specific size. - * - * @param string $size - * - * @return bool - */ - public function is_private_size( $size ) { - if ( AS3CF_Utils::is_full_size( $size ) ) { - return $this->is_private(); - } - - return in_array( $size, $this->private_sizes() ); - } - - /** - * Get the private prefix for attachment's private objects. + * @param string $object_key Size name + * @param string|null $bucket Optional bucket that ACL is potentially to be used with. * - * @return string + * @return string|null */ - public function private_prefix() { - $extra_info = $this->extra_info(); - - if ( ! empty( $extra_info['private_prefix'] ) ) { - return \AS3CF_Utils::trailingslash_prefix( $extra_info['private_prefix'] ); - } + public function get_acl_for_object_key( $object_key, $bucket = null ) { + /** @var Amazon_S3_And_CloudFront $as3cf */ + global $as3cf; - return ''; + return $as3cf->get_acl_for_intermediate_size( $this->source_id(), $object_key, $bucket, $this ); } /** @@ -390,7 +389,7 @@ public function private_prefix() { * offloaded: Count of offloaded media for site (current blog id) * not_offloaded: Difference between total and offloaded */ - public static function count_attachments( $skip_transient = false, $force = false ) { + public static function count_items( $skip_transient = false, $force = false ) { global $wpdb; $transient_key = 'as3cf_' . get_current_blog_id() . '_attachment_counts'; @@ -402,19 +401,17 @@ public static function count_attachments( $skip_transient = false, $force = fals } if ( $force || $skip_transient || false === ( $result = get_site_transient( $transient_key ) ) ) { - // We want to count distinct relative Media Library paths - // and ensure type is also attachment as other post types can use the same _wp_attached_file postmeta key. - $sql = " - SELECT COUNT(DISTINCT p.`ID`) total, COUNT(DISTINCT i.`id`) offloaded - FROM " . $wpdb->posts . " AS p - STRAIGHT_JOIN " . $wpdb->postmeta . " AS m ON p.ID = m.post_id AND m.`meta_key` = '_wp_attached_file' - LEFT OUTER JOIN " . static::items_table() . " AS i ON p.`ID` = i.`source_id` AND i.`source_type` = 'media-library' - WHERE p.`post_type` = 'attachment' - "; + // Simplified media counting + $sql = "SELECT count(id) FROM {$wpdb->posts} WHERE post_type = 'attachment'"; + $attachment_count = (int) $wpdb->get_var( $sql ); - $result = $wpdb->get_row( $sql, ARRAY_A ); + $sql = 'SELECT count(id) FROM ' . static::items_table() . ' WHERE source_type = %s'; + $sql = $wpdb->prepare( $sql, static::$source_type ); + $offloaded_count = (int) $wpdb->get_var( $sql ); - $result['not_offloaded'] = max( $result['total'] - $result['offloaded'], 0 ); + $result['total'] = $attachment_count; + $result['offloaded'] = $offloaded_count; + $result['not_offloaded'] = max( $attachment_count - $offloaded_count, 0 ); ksort( $result ); @@ -434,9 +431,9 @@ public static function count_attachments( $skip_transient = false, $force = fals * * While source id isn't strictly unique, it is by source type, which is always used in queries based on called class. * - * @param integer $upper_bound Returned source_ids should be lower than this, use null/0 for no upper bound. - * @param integer $limit Maximum number of source_ids to return. Required if not counting. - * @param bool $count Just return a count of matching source_ids? Negates $limit, default false. + * @param int $upper_bound Returned source_ids should be lower than this, use null/0 for no upper bound. + * @param int $limit Maximum number of source_ids to return. Required if not counting. + * @param bool $count Just return a count of matching source_ids? Negates $limit, default false. * * @return array|int */ @@ -486,74 +483,12 @@ public static function get_missing_source_ids( $upper_bound, $limit, $count = fa $sql = $wpdb->prepare( $sql, $args ); if ( $count ) { - return $wpdb->get_var( $sql ); + return (int) $wpdb->get_var( $sql ); } else { return array_map( 'intval', $wpdb->get_col( $sql ) ); } } - /** - * Search for all items that have the source path(s). - * - * @param array|string $paths Array of relative source paths. - * @param array|int $exclude_source_ids Array of source_ids to exclude from search. Default, none. - * @param bool $exact_match Use paths as supplied (true, default), or greedy match on path without extension (e.g. find edited too). - * @param bool $first_only Only return first matched item sorted by source_id. Default false. - * - * @return array - */ - public static function get_by_source_path( $paths, $exclude_source_ids = array(), $exact_match = true, $first_only = false ) { - global $wpdb; - - if ( ! is_array( $paths ) && is_string( $paths ) && ! empty( $paths ) ) { - $paths = array( $paths ); - } - - if ( ! is_array( $paths ) || empty( $paths ) ) { - return array(); - } - - $paths = \AS3CF_Utils::make_upload_file_paths_relative( $paths ); - - $args = array( static::$source_type ); - - $sql = ' - SELECT DISTINCT items.* - FROM ' . static::items_table() . ' AS items USE INDEX (uidx_source_path, uidx_original_source_path) - WHERE items.source_type = %s - '; - - if ( ! empty( $exclude_source_ids ) ) { - if ( ! is_array( $exclude_source_ids ) ) { - $exclude_source_ids = array( $exclude_source_ids ); - } - - $sql .= ' AND items.source_id NOT IN (' . join( ',', $exclude_source_ids ) . ')'; - } - - if ( $exact_match ) { - $sql .= " AND (items.source_path IN ('" . join( "','", $paths ) . "')"; - $sql .= " OR items.original_source_path IN ('" . join( "','", $paths ) . "'))"; - } else { - $likes = array_map( function ( $path ) { - $ext = '.' . pathinfo( $path, PATHINFO_EXTENSION ); - $path = substr_replace( $path, '%', -strlen( $ext ) ); - - return "items.source_path LIKE '" . $path . "' OR items.original_source_path LIKE '" . $path . "'"; - }, $paths ); - - $sql .= ' AND (' . join( ' OR ', $likes ) . ')'; - } - - if ( $first_only ) { - $sql .= ' ORDER BY items.source_id LIMIT 1'; - } - - $sql = $wpdb->prepare( $sql, $args ); - - return array_map( 'static::create', $wpdb->get_results( $sql ) ); - } - /** * Finds Media Library items with same source_path and sets them as offloaded. */ @@ -601,9 +536,183 @@ public function offload_duplicate_items() { $this->extra_info() ); $as3cf_item->save(); + $as3cf_item->duplicate_filesize_total( $this->source_id() ); } } + /** + * Returns a link to the items edit page in WordPress + * + * @param object $error + * + * @return object|null Object containing url and link text + */ + public static function admin_link( $error ) { + return (object) array( + 'url' => get_edit_post_link( $error->source_id ), + 'text' => __( 'Edit', 'amazon-s3-and-cloudfront' ), + ); + } + + /** + * Return a year/month string for the item + * + * @return string + */ + protected function get_item_time() { + return $this->get_attachment_folder_year_month(); + } + + /** + * Get the year/month string for attachment's upload. + * + * Fall back to post date if attached, otherwise current date. + * + * @param array $data + * + * @return string + */ + private function get_attachment_folder_year_month( $data = array() ) { + if ( empty( $data ) ) { + $data = wp_get_attachment_metadata( $this->source_id(), true ); + } + + if ( isset( $data['file'] ) ) { + $time = $this->get_folder_time_from_url( $data['file'] ); + } + + if ( empty( $time ) && ( $local_url = wp_get_attachment_url( $this->source_id() ) ) ) { + $time = $this->get_folder_time_from_url( $local_url ); + } + + if ( empty( $time ) ) { + $time = date( 'Y/m' ); + + if ( ! ( $attach = get_post( $this->source_id() ) ) ) { + return $time; + } + + if ( ! $attach->post_parent ) { + return $time; + } + + if ( ! ( $post = get_post( $attach->post_parent ) ) ) { + return $time; + } + + if ( substr( $post->post_date_gmt, 0, 4 ) > 0 ) { + return date( 'Y/m', strtotime( $post->post_date_gmt . ' +0000' ) ); + } + } + + return $time; + } + + /** + * Get the upload folder time from given URL + * + * @param string $url + * + * @return null|string YYYY/MM format. + */ + private function get_folder_time_from_url( $url ) { + if ( ! is_string( $url ) ) { + return null; + } + + preg_match( '@[0-9]{4}/[0-9]{2}/@', $url, $matches ); + + if ( isset( $matches[0] ) ) { + return untrailingslashit( $matches[0] ); + } + + return null; + } + + /** + * Update filesize and as3cf_filesize_total metadata on the underlying media library item + * after removing the local file. + * + * @param int $original_size + * @param int $total_size + */ + public function update_filesize_after_remove_local( $original_size, $total_size ) { + update_post_meta( $this->source_id(), 'as3cf_filesize_total', $total_size ); + + if ( 0 < $original_size && ( $data = get_post_meta( $this->source_id(), '_wp_attachment_metadata', true ) ) ) { + if ( empty( $data['filesize'] ) ) { + $data['filesize'] = $original_size; + + // Update metadata with filesize + update_post_meta( $this->source_id(), '_wp_attachment_metadata', $data ); + } + } + } + + /** + * Cleanup filesize and as3cf_filesize_total metadata on the underlying media library item + * after downloading a file back from the bucket + */ + public function update_filesize_after_download_local() { + $data = get_post_meta( $this->source_id(), '_wp_attachment_metadata', true ); + + /* + * Audio and video have a filesize added to metadata by default, but images and anything else don't. + * Note: Could have used `wp_generate_attachment_metadata` here to test whether default metadata has 'filesize', + * but it not only has side effects it also does a lot of work considering it's not a huge deal for this entry to hang around. + */ + if ( + ! empty( $data ) && + ( empty( $data['mime_type'] ) || + 0 === strpos( $data['mime_type'], 'image/' ) || + ! ( 0 === strpos( $data['mime_type'], 'audio/' ) || 0 === strpos( $data['mime_type'], 'video/' ) ) ) + ) { + unset( $data['filesize'] ); + update_post_meta( $this->source_id(), '_wp_attachment_metadata', $data ); + } + + delete_post_meta( $this->source_id(), 'as3cf_filesize_total' ); + } + + /** + * Duplicate 'as3cf_filesize_total' meta if it exists for an attachment. + * + * @param int $attachment_id + */ + public function duplicate_filesize_total( $attachment_id ) { + if ( ! ( $filesize = get_post_meta( $attachment_id, 'as3cf_filesize_total', true ) ) ) { + // No filesize to duplicate. + return; + } + + update_post_meta( $this->source_id(), 'as3cf_filesize_total', $filesize ); + } + + /** + * If another item in current site shares full size *local* paths, only remove remote files not referenced by duplicates. + * We reference local paths as they should be reflected one way or another remotely, including backups. + * + * @params Item $as3cf_item + * @params array $paths + */ + public function remove_duplicate_paths( Item $as3cf_item, $paths ) { + $full_size_paths = AS3CF_Utils::fullsize_paths( $as3cf_item->full_source_paths() ); + $as3cf_items_with_paths = static::get_by_source_path( $full_size_paths, array( $as3cf_item->source_id() ), false ); + + $duplicate_paths = array(); + + foreach ( $as3cf_items_with_paths as $as3cf_item_with_path ) { + /* @var Media_Library_Item $as3cf_item_with_path */ + $duplicate_paths += array_values( AS3CF_Utils::get_attachment_file_paths( $as3cf_item_with_path->source_id(), false, false, true ) ); + } + + if ( ! empty( $duplicate_paths ) ) { + $paths = array_diff( $paths, $duplicate_paths ); + } + + return $paths; + } + /* * >>> LEGACY ROUTINES BEGIN >>> */ @@ -611,8 +720,8 @@ public function offload_duplicate_items() { /** * Convert the provider info array for an attachment to item object. * - * @param integer $source_id - * @param array $provider_info + * @param int $source_id + * @param array $provider_info * * @return bool|Media_Library_Item */ @@ -631,7 +740,7 @@ private static function _legacy_provider_info_to_item( $source_id, $provider_inf $provider_info['region'], $provider_info['bucket'], $provider_info['key'], - isset( $provider_info['acl'] ) && false !== strpos( $provider_info['acl'], 'private' ) ? true : false, + isset( $provider_info['acl'] ) && false !== strpos( $provider_info['acl'], 'private' ), $source_id, $attached_file, wp_basename( $attached_file ), diff --git a/classes/items/remove-local-handler.php b/classes/items/remove-local-handler.php new file mode 100644 index 00000000..37dc838f --- /dev/null +++ b/classes/items/remove-local-handler.php @@ -0,0 +1,209 @@ + false, + 'provider_keys' => array(), + 'files_to_remove' => array(), + ); + } + + /** + * Create manifest for local removal. + * + * @param Item $as3cf_item + * @param array $options + * + * @return Manifest|WP_Error + */ + protected function pre_handle( Item $as3cf_item, array $options ) { + $manifest = new Manifest(); + $source_id = $as3cf_item->source_id(); + $primary_file = ''; + $files_to_remove = array(); + + // Note: Unable to use Item::full_size_paths() here + // as source item's metadata may not be up-to-date yet. + foreach ( $as3cf_item->objects() as $object_key => $object ) { + $file = $as3cf_item->full_source_path( $object_key ); + + if ( in_array( $file, $this->remove_blocked ) ) { + continue; + } + + if ( 0 < count( $options['files_to_remove'] ) && ! in_array( $file, $options['files_to_remove'] ) ) { + continue; + } + + // If needed, make sure this item exists among the provider keys. + if ( true === $options['verify_exists_on_provider'] ) { + if ( empty( $options['provider_keys'][ $source_id ] ) ) { + continue; + } + + if ( ! in_array( $as3cf_item->provider_key( $object_key ), $options['provider_keys'][ $source_id ] ) ) { + continue; + } + } + + if ( file_exists( $file ) ) { + $files_to_remove[] = $file; + + if ( Item::primary_object_key() === $object_key ) { + $primary_file = $file; + } + } + } + + /** + * Filters array of local files before being removed from server. + * + * @param array $files_to_remove Array of paths to be removed + * @param Item $as3cf_item The Item object + * @param array $item_source Item source descriptor array + */ + $filtered_files_to_remove = apply_filters( 'as3cf_remove_local_files', $files_to_remove, $as3cf_item, $as3cf_item->get_item_source_array() ); + + // Ensure fileset is unique and does not contain files already blocked. + $filtered_files_to_remove = array_unique( array_diff( $filtered_files_to_remove, $this->remove_blocked ) ); + + // If filter removes files from list, block attempts to remove them in later calls. + $this->remove_blocked = array_merge( $this->remove_blocked, array_diff( $files_to_remove, $filtered_files_to_remove ) ); + + foreach ( $filtered_files_to_remove as $file ) { + // Filter may have added some files to check for existence. + if ( ! in_array( $file, $files_to_remove ) ) { + if ( ! file_exists( $file ) ) { + continue; + } + } + + /** + * Filter individual files that might still be kept local. + * + * @param bool $preserve Should the file be kept on the server? + * @param string $file Full path to the local file + */ + if ( false !== apply_filters( 'as3cf_preserve_file_from_local_removal', false, $file ) ) { + $this->remove_blocked[] = $file; + continue; + } + + $manifest->objects[] = array( + 'file' => $file, + 'size' => filesize( $file ), + 'is_primary' => $file === $primary_file, + ); + } + + return $manifest; + } + + /** + * Delete local files described in the manifest object array. + * + * @param Item $as3cf_item + * @param Manifest $manifest + * @param array $options + * + * @return bool|WP_Error + */ + protected function handle_item( Item $as3cf_item, Manifest $manifest, array $options ) { + foreach ( $manifest->objects as &$file_to_remove ) { + $file = $file_to_remove['file']; + + $file_to_remove['remove_result'] = array( 'status' => self::STATUS_OK ); + + if ( ! @unlink( $file ) ) { + $this->remove_blocked[] = $file; + + $file_to_remove['remove_result']['status'] = self::STATUS_FAILED; + $file_to_remove['remove_result']['message'] = "Error removing local file at $file"; + + if ( ! file_exists( $file ) ) { + $file_to_remove['remove_result']['message'] = "Error removing local file. Couldn't find the file at $file"; + } else if ( ! is_writable( $file ) ) { + $file_to_remove['remove_result']['message'] = "Error removing local file. Ownership or permissions are mis-configured for $file"; + } + } + } + + return true; + } + + /** + * Perform post handle tasks. + * + * @param Item $as3cf_item + * @param Manifest $manifest + * @param array $options + * + * @return bool|WP_Error + */ + protected function post_handle( Item $as3cf_item, Manifest $manifest, array $options ) { + if ( empty( $manifest->objects ) ) { + return true; + } + + // Assume we didn't touch the primary file. + $this->removed_primary_size[ $as3cf_item->source_id() ] = 0; + + foreach ( $manifest->objects as $file_to_remove ) { + if ( $file_to_remove['remove_result']['status'] !== self::STATUS_OK ) { + AS3CF_Error::log( $file_to_remove['remove_result']['message'] ); + continue; + } + + if ( empty( $this->removed_size[ $as3cf_item->source_id() ] ) ) { + $this->removed_size[ $as3cf_item->source_id() ] = $file_to_remove['size']; + } else { + $this->removed_size[ $as3cf_item->source_id() ] += $file_to_remove['size']; + } + + if ( $file_to_remove['is_primary'] ) { + $this->removed_primary_size[ $as3cf_item->source_id() ] = $file_to_remove['size']; + } + } + + $as3cf_item->update_filesize_after_remove_local( $this->removed_primary_size[ $as3cf_item->source_id() ], $this->removed_size[ $as3cf_item->source_id() ] ); + + return true; + } +} \ No newline at end of file diff --git a/classes/items/remove-provider-handler.php b/classes/items/remove-provider-handler.php new file mode 100644 index 00000000..ab85d952 --- /dev/null +++ b/classes/items/remove-provider-handler.php @@ -0,0 +1,141 @@ + array(), + 'offloaded_files' => array(), + ); + } + + /** + * Create manifest for removal from provider. + * + * @param Item $as3cf_item + * @param array $options + * + * @return Manifest|WP_Error + */ + protected function pre_handle( Item $as3cf_item, array $options ) { + $manifest = new Manifest(); + $paths = array(); + + if ( ! empty( $options['object_keys'] ) && ! is_array( $options['object_keys'] ) ) { + return new WP_Error( 'remove-error', __( 'Invalid object_keys option provided.', 'amazon-s3-and-cloudfront' ) ); + } + + if ( ! empty( $options['offloaded_files'] ) && ! is_array( $options['offloaded_files'] ) ) { + return new WP_Error( 'remove-error', __( 'Invalid offloaded_files option provided.', 'amazon-s3-and-cloudfront' ) ); + } + + if ( ! empty( $options['object_keys'] ) && ! empty( $options['offloaded_files'] ) ) { + return new WP_Error( 'remove-error', __( 'Providing both object_keys and offloaded_files options is not supported.', 'amazon-s3-and-cloudfront' ) ); + } + + if ( empty( $options['offloaded_files'] ) ) { + foreach ( $as3cf_item->objects() as $object_key => $object ) { + if ( 0 < count( $options['object_keys'] ) && ! in_array( $object_key, $options['object_keys'] ) ) { + continue; + } + $paths[ $object_key ] = $as3cf_item->full_source_path( $object_key ); + } + } else { + foreach ( $options['offloaded_files'] as $filename => $object ) { + $paths[ $filename ] = $as3cf_item->full_source_path_for_filename( $filename ); + } + } + + /** + * Filters array of source files before being removed from provider. + * + * @param array $paths Array of local paths to be removed from provider + * @param Item $as3cf_item The Item object + * @param array $item_source The item source descriptor array + */ + $paths = apply_filters( 'as3cf_remove_source_files_from_provider', $paths, $as3cf_item, $as3cf_item->get_item_source_array() ); + $paths = array_unique( $paths ); + + // Remove local source paths that other items may have offloaded. + $paths = $as3cf_item->remove_duplicate_paths( $as3cf_item, $paths ); + + // Nothing to do, shortcut out. + if ( empty( $paths ) ) { + return $manifest; + } + + if ( empty( $options['offloaded_files'] ) ) { + foreach ( $paths as $object_key => $path ) { + $manifest->objects[] = array( + 'Key' => $as3cf_item->provider_key( $object_key ), + ); + } + } else { + foreach ( $paths as $filename => $path ) { + $manifest->objects[] = array( + 'Key' => $as3cf_item->provider_key_for_filename( $filename, $options['offloaded_files'][ $filename ]['is_private'] ), + ); + } + } + + return $manifest; + } + + /** + * Delete provider objects described in the manifest object array + * + * @param Item $as3cf_item + * @param Manifest $manifest + * @param array $options + * + * @return bool|WP_Error + */ + protected function handle_item( Item $as3cf_item, Manifest $manifest, array $options ) { + $chunks = array_chunk( $manifest->objects, 1000 ); + $region = $as3cf_item->region(); + $bucket = $as3cf_item->bucket(); + + try { + foreach ( $chunks as $chunk ) { + $this->as3cf->get_provider_client( $region )->delete_objects( array( + 'Bucket' => $bucket, + 'Objects' => $chunk, + ) ); + } + } catch ( Exception $e ) { + AS3CF_Error::log( 'Error removing files from bucket: ' . $e->getMessage() ); + + return new WP_Error( 'remove-error', $e->getMessage() ); + } + + return true; + } + + /** + * Perform post handle tasks. + * + * @param Item $as3cf_item + * @param Manifest $manifest + * @param array $options + * + * @return bool|WP_Error + */ + protected function post_handle( Item $as3cf_item, Manifest $manifest, array $options ) { + return true; + } +} \ No newline at end of file diff --git a/classes/items/upload-handler.php b/classes/items/upload-handler.php new file mode 100644 index 00000000..9f6c570b --- /dev/null +++ b/classes/items/upload-handler.php @@ -0,0 +1,449 @@ + array(), + ); + } + + /** + * Prepare item for uploading by running filters, updating + * + * @param Item $as3cf_item + * @param array $options + * + * @return Manifest|WP_Error + */ + protected function pre_handle( Item $as3cf_item, array $options ) { + $manifest = new Manifest(); + $source_type_name = $this->as3cf->get_source_type_name( $as3cf_item->source_type() ); + $primary_key = Item::primary_object_key(); + + // Check for valid file path before attempting upload + if ( empty( $as3cf_item->source_path() ) ) { + $error_msg = sprintf( __( '%s with id %d does not have a valid file path', 'amazon-s3-and-cloudfront' ), $source_type_name, $as3cf_item->source_id() ); + + return $this->return_handler_error( $error_msg ); + } + + // Ensure path is a string + if ( ! is_string( $as3cf_item->source_path() ) ) { + $error_msg = sprintf( __( '%s with id %d. Provided path is not a string', 'amazon-s3-and-cloudfront' ), $source_type_name, $as3cf_item->source_id() ); + + return $this->return_handler_error( $error_msg ); + } + + // Ensure primary source file exists for new offload. + if ( empty( $as3cf_item->id() ) && ! file_exists( $as3cf_item->full_source_path( $primary_key ) ) ) { + $error_msg = sprintf( __( 'Primary file %s does not exist', 'amazon-s3-and-cloudfront' ), $as3cf_item->full_source_path( $primary_key ) ); + + return $this->return_handler_error( $error_msg ); + } + + // Get primary file's stats. + $file_name = wp_basename( $as3cf_item->source_path() ); + $file_type = wp_check_filetype_and_ext( $as3cf_item->source_path(), $file_name ); + $allowed_types = $this->as3cf->get_allowed_mime_types(); + + // check mime type of file is in allowed provider mime types + if ( ! in_array( $file_type['type'], $allowed_types, true ) ) { + $error_msg = sprintf( __( 'Mime type %s is not allowed', 'amazon-s3-and-cloudfront' ), $file_type['type'] ); + + return $this->return_handler_error( $error_msg ); + } + + $default_acl = $this->as3cf->get_storage_provider()->get_default_acl(); + $private_acl = $this->as3cf->get_storage_provider()->get_private_acl(); + + foreach ( $as3cf_item->objects() as $object_key => $object ) { + // Avoid attempting uploading to an item that doesn't have the primary file in place. + if ( $primary_key !== $object_key && empty( $as3cf_item->id() ) && ! isset( $manifest->objects[ $primary_key ] ) ) { + continue; + } + + $source_path = $as3cf_item->full_source_path( $object_key ); + + // If the file has already been offloaded, + // don't try and (fail to) re-offload if the file isn't available. + if ( $this->in_offloaded_files( $object['source_file'], $options ) && ! file_exists( $source_path ) ) { + continue; + } + + /** + * This filter allows you to change the public/private status of an individual file associated + * with an uploaded item before it's uploaded to the provider. + * + * @param bool $is_private Should the object be private? + * @param string $object_key A unique file identifier for a composite item, e.g. image's "size" such as full, small, medium, large. + * @param Item $as3cf_item The item being uploaded. + * + * @return bool + */ + $is_private = apply_filters( 'as3cf_upload_object_key_as_private', $as3cf_item->is_private( $object_key ), $object_key, $as3cf_item ); + $as3cf_item->set_is_private( $is_private, $object_key ); + + $object_acl = $as3cf_item->is_private( $object_key ) ? $private_acl : $default_acl; + + $args = array( + 'Bucket' => $as3cf_item->bucket(), + 'Key' => $as3cf_item->path( $object_key ), + 'SourceFile' => $source_path, + 'ContentType' => AS3CF_Utils::get_mime_type( $object['source_file'] ), + 'CacheControl' => 'max-age=31536000', + ); + + // Only set ACL if actually required, some storage provider and bucket settings disable changing ACL. + if ( ! empty( $object_acl ) && $this->as3cf->use_acl_for_intermediate_size( 0, $object_key, $as3cf_item->bucket(), $as3cf_item ) ) { + $args['ACL'] = $object_acl; + } + + // TODO: Remove GZIP functionality. + // Handle gzip on supported items + if ( + $this->should_gzip_file( $source_path, $as3cf_item->source_type() ) && + false !== ( $gzip_body = gzencode( file_get_contents( $source_path ) ) ) + ) { + unset( $args['SourceFile'] ); + + $args['Body'] = $gzip_body; + $args['ContentEncoding'] = 'gzip'; + } + + /** + * This filter allows you to change the arguments passed to the cloud storage SDK client when + * offloading a file to the bucket. + * + * Note: It is possible to change the destination 'Bucket' only while processing the primary object_key. + * All other object_keys will use the same bucket as the item's primary object. + * The 'Key' should be the "public" Key path. If a private prefix is configured + * for use with signed CloudFront URLs or similar, that prefix will be added later. + * A change to the 'Key' will only be handled when processing the primary object key. + * + * @param array $args Information to be sent to storage provider during offload (e.g. PutObject) + * @param int $source_id Original file's unique ID for its source type + * @param string $object_key A unique file identifier for a composite item, e.g. image's "size" such as full, small, medium, large + * @param bool $copy True if the object is being copied between buckets + * @param array $item_source Item source array containing source type and id + * + * @return array + */ + $args = apply_filters( 'as3cf_object_meta', $args, $as3cf_item->source_id(), $object_key, false, $as3cf_item->get_item_source_array() ); + + // If the bucket is changed by the filter while processing the primary object, + // we should try and use that bucket for the item. + // If the bucket name is invalid, revert to configured bucket but log it. + // We don't abort here as ephemeral filesystems need to be accounted for, + // and the configured bucket is at least known to work. + if ( $primary_key === $object_key && $as3cf_item->bucket() !== $args['Bucket'] && empty( $as3cf_item->id() ) ) { + $bucket = $this->as3cf->check_bucket( $args['Bucket'] ); + + if ( $bucket ) { + $region = $this->as3cf->get_bucket_region( $bucket, true ); + + if ( is_wp_error( $region ) ) { + unset( $region ); + } + } + + if ( empty( $bucket ) || empty( $region ) ) { + $mesg = sprintf( + __( 'Bucket name "%1$s" is invalid, using "%2$s" instead.', 'amazon-s3-and-cloudfront' ), + $args['Bucket'], + $as3cf_item->bucket() + ); + AS3CF_Error::log( $mesg ); + $args['Bucket'] = $as3cf_item->bucket(); + } else { + $args['Bucket'] = $bucket; + $as3cf_item->set_bucket( $bucket ); + $as3cf_item->set_region( $region ); + } + + unset( $bucket, $region ); + } elseif ( $primary_key === $object_key && $as3cf_item->bucket() !== $args['Bucket'] && ! empty( $as3cf_item->id() ) ) { + $args['Bucket'] = $as3cf_item->bucket(); + AS3CF_Error::log( __( 'The bucket may not be changed via filters for a previously offloaded item.', 'amazon-s3-and-cloudfront' ) ); + } elseif ( $primary_key !== $object_key && $as3cf_item->bucket() !== $args['Bucket'] ) { + $args['Bucket'] = $as3cf_item->bucket(); + } + + // If the Key has been changed for the primary object key, then that should be reflected in the item. + if ( $primary_key === $object_key && $as3cf_item->path( $object_key ) !== $args['Key'] && empty( $as3cf_item->id() ) ) { + $prefix = AS3CF_Utils::trailingslash_prefix( dirname( $args['Key'] ) ); + + if ( $prefix === '.' ) { + $prefix = ''; + } + + $as3cf_item->update_path_prefix( $prefix ); + + // If the filter tried to use a different filename too, log it. + if ( wp_basename( $args['Key'] ) !== wp_basename( $as3cf_item->path( $object_key ) ) ) { + $mesg = sprintf( + __( 'The offloaded filename must not be changed, "%1$s" has been used instead of "%2$s".', 'amazon-s3-and-cloudfront' ), + wp_basename( $as3cf_item->path( $object_key ) ), + wp_basename( $args['Key'] ) + ); + AS3CF_Error::log( $mesg ); + } + } elseif ( $primary_key === $object_key && $as3cf_item->path( $object_key ) !== $args['Key'] && ! empty( $as3cf_item->id() ) ) { + $args['Key'] = $as3cf_item->path( $object_key ); + AS3CF_Error::log( __( 'The key may not be changed via filters for a previously offloaded item.', 'amazon-s3-and-cloudfront' ) ); + } elseif ( $primary_key !== $object_key && $as3cf_item->path( $object_key ) !== $args['Key'] ) { + $args['Key'] = $as3cf_item->path( $object_key ); + } + + // If ACL has been set, does the object's is_private need updating? + $is_private = ! empty( $args['ACL'] ) && $private_acl === $args['ACL'] || $as3cf_item->is_private( $object_key ); + $as3cf_item->set_is_private( $is_private, $object_key ); + + // Protect against filter use and only set ACL if actually required, some storage provider and bucket settings disable changing ACL. + if ( isset( $args['ACL'] ) && ! $this->as3cf->use_acl_for_intermediate_size( 0, $object_key, $as3cf_item->bucket(), $as3cf_item ) ) { + unset( $args['ACL'] ); + } + + // Adjust the actual Key to add the private prefix before uploading. + if ( $as3cf_item->is_private( $object_key ) ) { + $args['Key'] = $as3cf_item->provider_key( $object_key ); + } + + // If we've already attempted to offload this source file, leave it out of the manifest. + if ( in_array( md5( serialize( $args ) ), $this->attempted_upload ) ) { + continue; + } + + if ( $primary_key === $object_key ) { + /** + * Actions fires when an Item's primary file might be offloaded. + * + * This action gives notice that an Item is being processed for upload to a bucket, + * and the given arguments represent the primary file's potential offload location. + * However, if the current process is for picking up extra files associated with the item, + * the indicated primary file may not actually be offloaded if it does not exist + * on the server but has already been offloaded. + * + * @param Item $as3cf_item The Item whose files are being offloaded. + * @param array $args The arguments that could be used to offload the primary file. + */ + do_action( 'as3cf_pre_upload_object', $as3cf_item, $args ); + } + + $manifest->objects[ $object_key ]['args'] = $args; + } + + return $manifest; + } + + /** + * Upload item files to remote storage provider + * + * @param Item $as3cf_item + * @param Manifest $manifest + * @param array $options + * + * @return bool|WP_Error + */ + protected function handle_item( Item $as3cf_item, Manifest $manifest, array $options ) { + try { + $provider_client = $this->as3cf->get_provider_client( $as3cf_item->region() ); + } catch ( Exception $e ) { + return $this->return_handler_error( $e->getMessage() ); + } + + foreach ( $manifest->objects as $object_key => &$object ) { + $args = $object['args']; + + $object['upload_result'] = array( + 'status' => null, + 'message' => null, + ); + + if ( ! file_exists( $args['SourceFile'] ) ) { + $error_msg = sprintf( __( 'File %s does not exist', 'amazon-s3-and-cloudfront' ), $args['SourceFile'] ); + + $object['upload_result']['status'] = self::STATUS_FAILED; + $object['upload_result']['message'] = $error_msg; + + // If the missing source file is the primary file, abort the whole process. + if ( Item::primary_object_key() === $object_key ) { + return false; + } + + continue; + } + + $this->attempted_upload[] = md5( serialize( $args ) ); + + // Try to do the upload + try { + $provider_client->upload_object( $args ); + + $object['upload_result']['status'] = self::STATUS_OK; + } catch ( Exception $e ) { + $error_msg = sprintf( __( 'Error offloading %1$s to provider: %2$s', 'amazon-s3-and-cloudfront' ), $args['SourceFile'], $e->getMessage() ); + + $object['upload_result']['status'] = self::STATUS_FAILED; + $object['upload_result']['message'] = $error_msg; + } + } + + return true; + } + + /** + * Handle local housekeeping after uploads. + * + * @param Item $as3cf_item + * @param Manifest $manifest + * @param array $options + * + * @return bool|WP_Error + */ + protected function post_handle( Item $as3cf_item, Manifest $manifest, array $options ) { + $item_objects = $as3cf_item->objects(); + $errors = new WP_Error; + $i = 1; + + // Reconcile the Item's objects with their manifest status. + foreach ( $item_objects as $object_key => $object ) { + // If there was no attempt made to offload the file, + // then remove it from list of offloaded objects. + // However, if the source file has previously been offloaded, + // we should just skip any further processing of it + // as the associated objects are still offloaded. + if ( ! isset( $manifest->objects[ $object_key ]['upload_result']['status'] ) ) { + if ( empty( $options['offloaded_files'][ $object['source_file'] ] ) ) { + unset( $item_objects[ $object_key ] ); + } + continue; + } + + // If the upload didn't succeed, we need to remove the object/size from the item. + // However, if the source file has previously been offloaded, we should just log the error. + if ( $manifest->objects[ $object_key ]['upload_result']['status'] !== self::STATUS_OK ) { + if ( empty( $options['offloaded_files'][ $object['source_file'] ] ) ) { + unset( $item_objects[ $object_key ] ); + } + $errors->add( 'upload-object-' . $i++, $manifest->objects[ $object_key ]['upload_result']['message'] ); + } + } + + // Set the potentially changed list of offloaded objects. + $as3cf_item->set_objects( $item_objects ); + + // Only save if we have the primary file uploaded. + if ( isset( $item_objects[ Item::primary_object_key() ] ) ) { + $as3cf_item->save(); + } + + /** + * Fires action after uploading finishes + * + * @param Item $as3cf_item The item that was just uploaded + */ + do_action( 'as3cf_post_upload_item', $as3cf_item ); + + if ( count( $errors->get_error_codes() ) ) { + return $errors; + } + + return true; + } + + /** + * Should gzip file + * + * @param string $file_path + * @param string $source_type + * + * @return bool + */ + protected function should_gzip_file( $file_path, $source_type ) { + $file_type = wp_check_filetype_and_ext( $file_path, $file_path ); + $mimes = $this->get_mime_types_to_gzip( $source_type ); + + if ( in_array( $file_type, $mimes ) && is_readable( $file_path ) ) { + return true; + } + + return false; + } + + /** + * Get mime types to gzip + * + * @param string $source_type + * + * @return array + */ + protected function get_mime_types_to_gzip( $source_type ) { + /** + * Return array of mime types that needs to be gzipped before upload + * + * @param array $mime_types The array of mime types + * @param bool $media_library If the uploaded file is part of the media library + * @param string $source_type The source type of the uploaded item + */ + return apply_filters( + 'as3cf_gzip_mime_types', + array( + 'css' => 'text/css', + 'eot' => 'application/vnd.ms-fontobject', + 'html' => 'text/html', + 'ico' => 'image/x-icon', + 'js' => 'application/javascript', + 'json' => 'application/json', + 'otf' => 'application/x-font-opentype', + 'rss' => 'application/rss+xml', + 'svg' => 'image/svg+xml', + 'ttf' => 'application/x-font-ttf', + 'woff' => 'application/font-woff', + 'woff2' => 'application/font-woff2', + 'xml' => 'application/xml', + ), + 'media_library' === $source_type, + $source_type + ); + } + + /** + * Has the given file name already been offloaded? + * + * @param string $filename + * @param array $options + * + * @return bool + */ + private function in_offloaded_files( $filename, $options ) { + if ( empty( $options['offloaded_files'] ) ) { + return false; + } + + return array_key_exists( $filename, $options['offloaded_files'] ); + } +} \ No newline at end of file diff --git a/classes/providers/storage/storage-provider.php b/classes/providers/storage/storage-provider.php index 82fa3e1d..c49980db 100644 --- a/classes/providers/storage/storage-provider.php +++ b/classes/providers/storage/storage-provider.php @@ -572,11 +572,12 @@ public function get_client( array $args, $force = false ) { /** * Get object keys from multiple clients. * - * @param array $regions + * @param array $regions + * @param string $source_type * * @return array */ - public static function get_keys_from_regions( array $regions ) { + public static function get_keys_from_regions( array $regions, $source_type ) { $keys = array(); foreach ( $regions as $region ) { @@ -591,7 +592,7 @@ public static function get_keys_from_regions( array $regions ) { if ( ! empty( $region_keys ) ) { foreach ( $region_keys as $attachment_id => $found_keys ) { - $keys[ $attachment_id ] = AS3CF_Utils::validate_attachment_keys( $attachment_id, $found_keys ); + $keys[ $attachment_id ] = AS3CF_Utils::validate_attachment_keys( $attachment_id, $found_keys, $source_type ); } } } diff --git a/classes/upgrades/upgrade-edd-replace-urls.php b/classes/upgrades/upgrade-edd-replace-urls.php index 6759109e..0b957e91 100644 --- a/classes/upgrades/upgrade-edd-replace-urls.php +++ b/classes/upgrades/upgrade-edd-replace-urls.php @@ -2,6 +2,8 @@ namespace DeliciousBrains\WP_Offload_Media\Upgrades; +use DeliciousBrains\WP_Offload_Media\Items\Media_Library_Item; + /** * Upgrade_EDD_Replace_URLs Class * @@ -82,7 +84,12 @@ protected function upgrade_item( $item ) { continue; } - if ( $url = $this->as3cf->get_attachment_local_url( $attachment['attachment_id'] ) ) { + $as3cf_item = Media_Library_Item::get_by_source_id( $attachment['attachment_id'] ); + if ( empty( $as3cf_item ) ) { + continue; + } + + if ( $url = $as3cf_item->get_local_url() ) { $attachments[ $key ]['file'] = $url; } } diff --git a/classes/upgrades/upgrade-file-sizes.php b/classes/upgrades/upgrade-file-sizes.php index 03cf5d82..c865c3f8 100644 --- a/classes/upgrades/upgrade-file-sizes.php +++ b/classes/upgrades/upgrade-file-sizes.php @@ -52,24 +52,25 @@ protected function get_running_update_text() { /** * Get the total file sizes for an attachment and associated files. * - * @param mixed $attachment + * @param mixed $item * * @return bool + * @throws Exception */ - protected function upgrade_item( $attachment ) { - $provider_object = unserialize( $attachment->provider_object ); + protected function upgrade_item( $item ) { + $provider_object = unserialize( $item->provider_object ); if ( false === $provider_object ) { - AS3CF_Error::log( 'Failed to unserialize offload meta for attachment ' . $attachment->ID . ': ' . $attachment->provider_object ); + AS3CF_Error::log( 'Failed to unserialize offload meta for attachment ' . $item->ID . ': ' . $item->provider_object ); $this->error_count++; return false; } // Using Media_Library_Item::get_by_source_id falls back to legacy metadata and substitutes in defaults and potentially missing values. - $as3cf_item = Media_Library_Item::get_by_source_id( $attachment->ID ); + $as3cf_item = Media_Library_Item::get_by_source_id( $item->ID ); if ( ! $as3cf_item ) { - AS3CF_Error::log( 'Could not construct item for attachment with ID ' . $attachment->ID . ' from legacy offload metadata.' ); + AS3CF_Error::log( 'Could not construct item for attachment with ID ' . $item->ID . ' from legacy offload metadata.' ); $this->error_count++; return false; @@ -96,7 +97,7 @@ protected function upgrade_item( $attachment ) { // List objects for the attachment $result = $provider_client->list_objects( $args ); } catch ( Exception $e ) { - AS3CF_Error::log( 'Error listing objects of prefix ' . $search_prefix . ' for attachment ' . $attachment->ID . ' in bucket: ' . $e->getMessage() ); + AS3CF_Error::log( 'Error listing objects of prefix ' . $search_prefix . ' for attachment ' . $item->ID . ' in bucket: ' . $e->getMessage() ); $this->error_count++; return false; @@ -124,19 +125,19 @@ protected function upgrade_item( $attachment ) { } if ( 0 === $file_size_total ) { - AS3CF_Error::log( 'Total file size for the attachment is 0: ' . $attachment->ID ); + AS3CF_Error::log( 'Total file size for the attachment is 0: ' . $item->ID ); $this->error_count++; return false; } // Update the main file size for the attachment - $meta = get_post_meta( $attachment->ID, '_wp_attachment_metadata', true ); + $meta = get_post_meta( $item->ID, '_wp_attachment_metadata', true ); $meta['filesize'] = $main_file_size; - update_post_meta( $attachment->ID, '_wp_attachment_metadata', $meta ); + update_post_meta( $item->ID, '_wp_attachment_metadata', $meta ); // Add the total file size for all image sizes - update_post_meta( $attachment->ID, 'wpos3_filesize_total', $file_size_total ); + update_post_meta( $item->ID, 'wpos3_filesize_total', $file_size_total ); return true; } diff --git a/classes/upgrades/upgrade-filter-post.php b/classes/upgrades/upgrade-filter-post.php index 1f9a7f9a..eefdbd55 100644 --- a/classes/upgrades/upgrade-filter-post.php +++ b/classes/upgrades/upgrade-filter-post.php @@ -3,6 +3,7 @@ namespace DeliciousBrains\WP_Offload_Media\Upgrades; use AS3CF_Utils; +use DeliciousBrains\WP_Offload_Media\Items\Item; use DeliciousBrains\WP_Offload_Media\Upgrades\Exceptions\Batch_Limits_Exceeded_Exception; use DeliciousBrains\WP_Offload_Media\Upgrades\Exceptions\Too_Many_Errors_Exception; @@ -110,37 +111,34 @@ protected function close_session() { } /** - * Upgrade attachment. + * Upgrade item. * - * @param mixed $attachment + * @param mixed $item * * @return bool * @throws Batch_Limits_Exceeded_Exception * @throws Too_Many_Errors_Exception * */ - protected function upgrade_item( $attachment ) { + protected function upgrade_item( $item ) { $limit = apply_filters( 'as3cf_update_' . $this->upgrade_name . '_sql_limit', 100000 ); $where_highest_id = $this->last_post_id; $where_lowest_id = max( $where_highest_id - $limit, 0 ); while ( true ) { - $this->find_and_replace_attachment_urls( $attachment->ID, $where_lowest_id, $where_highest_id ); + $this->find_and_replace_attachment_urls( $item->ID, $where_lowest_id, $where_highest_id ); if ( $where_lowest_id <= 0 ) { // Batch completed return true; } - $where_highest_id = $where_lowest_id; - $where_lowest_id = max( $where_lowest_id - $limit, 0 ); + $where_highest_id = $where_lowest_id; + $where_lowest_id = max( $where_lowest_id - $limit, 0 ); + $this->last_post_id = $where_lowest_id; $this->check_batch_limits(); } - - $this->last_post_id = $where_lowest_id; - - return false; } /** @@ -161,12 +159,17 @@ protected function item_upgrade_completed( $item ) { * @param int $where_highest_id */ protected function find_and_replace_attachment_urls( $attachment_id, $where_lowest_id, $where_highest_id ) { - $meta = wp_get_attachment_metadata( $attachment_id, true ); - $backups = get_post_meta( $attachment_id, '_wp_attachment_backup_sizes', true ); - $file_path = get_attached_file( $attachment_id, true ); + $meta = wp_get_attachment_metadata( $attachment_id, true ); + $backups = get_post_meta( $attachment_id, '_wp_attachment_backup_sizes', true ); + $file_path = get_attached_file( $attachment_id, true ); + $as3cf_item = Item::get_by_source_id( $attachment_id ); + + if ( empty( $as3cf_item ) ) { + return; + } - $new_url = $this->as3cf->get_attachment_local_url( $attachment_id ); - $old_url = $this->as3cf->maybe_remove_query_string( $this->as3cf->get_attachment_url( $attachment_id, null, null, $meta, array(), true ) ); + $new_url = $as3cf_item->get_local_url(); + $old_url = $this->as3cf->maybe_remove_query_string( $as3cf_item->get_provider_url() ); if ( empty( $old_url ) || empty( $new_url ) ) { return; diff --git a/classes/upgrades/upgrade-item-extra-data.php b/classes/upgrades/upgrade-item-extra-data.php new file mode 100644 index 00000000..f18ecffb --- /dev/null +++ b/classes/upgrades/upgrade-item-extra-data.php @@ -0,0 +1,152 @@ +source_id ); + + if ( ! $as3cf_item ) { + AS3CF_Error::log( 'Could not construct item for attachment with ID ' . $item->source_id . '.' ); + $this->error_count++; + + return false; + } + + $result = $as3cf_item->save(); + + if ( is_wp_error( $result ) ) { + AS3CF_Error::log( 'Error saving item: ' . $result->get_error_message() ); + $this->error_count++; + + return false; + } + + return true; + } + + /** + * Get a count of all items to be processed. + * for the whole site + * + * @return int + */ + protected function count_items_to_process() { + return $this->count_items_with_old_extra_info( $this->blog_prefix ); + } + + /** + * Get all items to be processed. + * + * @param string $prefix Table prefix for blog. + * @param int $limit + * @param bool|mixed $offset + * + * @return array + */ + protected function get_items_to_process( $prefix, $limit, $offset = false ) { + $attachments = $this->get_items_with_old_extra_info( $prefix, false, $limit ); + + return $attachments; + } + + /** + * Get a count of items that have legacy extra info. + * + * @param string $prefix Table prefix for blog. + * + * @return int + */ + protected function count_items_with_old_extra_info( $prefix ) { + $count = $this->get_items_with_old_extra_info( $prefix, true ); + + return $count; + } + + /** + * Wrapper for database call to get items with legacy extra info. + * + * @param string $prefix Table prefix for blog. + * @param bool $count return count of attachments + * @param null|int $limit + * + * @return mixed + */ + protected function get_items_with_old_extra_info( $prefix, $count = false, $limit = null ) { + global $wpdb; + + $table = Item::ITEMS_TABLE; + + $sql = " + FROM {$prefix}{$table} + WHERE extra_info NOT LIKE '%s:7:\"objects\"%' AND source_type='media-library' + "; + + if ( $count ) { + $sql = 'SELECT COUNT(source_id)' . $sql; + + return $wpdb->get_var( $sql ); + } + + $sql = 'SELECT source_id' . $sql; + $sql .= ' ORDER BY id'; + + if ( $limit && $limit > 0 ) { + $sql .= sprintf( ' LIMIT %d', (int) $limit ); + } + + return $wpdb->get_results( $sql, OBJECT ); + } +} \ No newline at end of file diff --git a/classes/upgrades/upgrade-items-table.php b/classes/upgrades/upgrade-items-table.php index c5b34511..e2c2ab46 100644 --- a/classes/upgrades/upgrade-items-table.php +++ b/classes/upgrades/upgrade-items-table.php @@ -12,7 +12,6 @@ namespace DeliciousBrains\WP_Offload_Media\Upgrades; use AS3CF_Error; -use DeliciousBrains\WP_Offload_Media\Items\Item; use DeliciousBrains\WP_Offload_Media\Items\Media_Library_Item; /** @@ -51,23 +50,23 @@ protected function get_running_update_text() { /** * Move an attachment's provider object data from the postmeta table to the custom as3cf_objects table. * - * @param mixed $attachment + * @param mixed $item * * @return bool */ - protected function upgrade_item( $attachment ) { + protected function upgrade_item( $item ) { // Make sure legacy metadata isn't broken. - $provider_object = unserialize( $attachment->provider_object ); + $provider_object = unserialize( $item->provider_object ); if ( false === $provider_object ) { - AS3CF_Error::log( 'Failed to unserialize legacy offload metadata for attachment ' . $attachment->ID . ': ' . $attachment->provider_object ); + AS3CF_Error::log( 'Failed to unserialize legacy offload metadata for attachment ' . $item->ID . ': ' . $item->provider_object ); $this->error_count++; return false; } - if ( empty( $attachment->source_path ) ) { - AS3CF_Error::log( 'Attachment with ID ' . $attachment->ID . ' with legacy offload metadata has no local file path.' ); + if ( empty( $item->source_path ) ) { + AS3CF_Error::log( 'Attachment with ID ' . $item->ID . ' with legacy offload metadata has no local file path.' ); $this->error_count++; return false; @@ -77,10 +76,10 @@ protected function upgrade_item( $attachment ) { // If we're here we already know there's legacy metadata and that there isn't a new items table record yet, // or there's legacy metadata and an existing items table record that we can just re-save without issue before deleting legacy metadata. // An existing items table entry takes precedence over legacy metadata to avoid accidental overrides from migrations, custom code or other plugins. - $as3cf_item = Media_Library_Item::get_by_source_id( $attachment->ID ); + $as3cf_item = Media_Library_Item::get_by_source_id( $item->ID ); if ( ! $as3cf_item ) { - AS3CF_Error::log( 'Could not construct item for attachment with ID ' . $attachment->ID . ' from legacy offload metadata.' ); + AS3CF_Error::log( 'Could not construct item for attachment with ID ' . $item->ID . ' from legacy offload metadata.' ); $this->error_count++; return false; @@ -96,7 +95,7 @@ protected function upgrade_item( $attachment ) { } // Delete old metadata. - return delete_post_meta( $attachment->ID, 'amazonS3_info' ); + return delete_post_meta( $item->ID, 'amazonS3_info' ); } /** diff --git a/classes/upgrades/upgrade-meta-wp-error.php b/classes/upgrades/upgrade-meta-wp-error.php index 25431341..76296864 100644 --- a/classes/upgrades/upgrade-meta-wp-error.php +++ b/classes/upgrades/upgrade-meta-wp-error.php @@ -52,20 +52,20 @@ protected function get_running_update_text() { /** * Rebuild the attachment metadata for an attachment * - * @param mixed $attachment + * @param mixed $item * * @return bool */ - protected function upgrade_item( $attachment ) { - $provider_object = unserialize( $attachment->provider_object ); + protected function upgrade_item( $item ) { + $provider_object = unserialize( $item->provider_object ); if ( false === $provider_object ) { - AS3CF_Error::log( 'Failed to unserialize offload meta for attachment ' . $attachment->ID . ': ' . $attachment->provider_object ); + AS3CF_Error::log( 'Failed to unserialize offload meta for attachment ' . $item->ID . ': ' . $item->provider_object ); $this->error_count++; return false; } - $file = get_attached_file( $attachment->ID, true ); + $file = get_attached_file( $item->ID, true ); if ( ! file_exists( $file ) ) { // Copy back the file to the server if doesn't exist so we can successfully @@ -85,11 +85,11 @@ protected function upgrade_item( $attachment ) { } // Remove corrupted meta - delete_post_meta( $attachment->ID, '_wp_attachment_metadata' ); + delete_post_meta( $item->ID, '_wp_attachment_metadata' ); require_once ABSPATH . '/wp-admin/includes/image.php'; // Generate new attachment meta - wp_update_attachment_metadata( $attachment->ID, wp_generate_attachment_metadata( $attachment->ID, $file ) ); + wp_update_attachment_metadata( $item->ID, wp_generate_attachment_metadata( $item->ID, $file ) ); return true; } diff --git a/classes/upgrades/upgrade-region-meta.php b/classes/upgrades/upgrade-region-meta.php index 8e96cab3..445cea0e 100644 --- a/classes/upgrades/upgrade-region-meta.php +++ b/classes/upgrades/upgrade-region-meta.php @@ -50,24 +50,24 @@ protected function get_running_update_text() { /** * Get the region for the bucket where an attachment is located, update the S3 meta. * - * @param mixed $attachment + * @param mixed $item * * @return bool */ - protected function upgrade_item( $attachment ) { - $provider_object = unserialize( $attachment->provider_object ); + protected function upgrade_item( $item ) { + $provider_object = unserialize( $item->provider_object ); if ( false === $provider_object ) { - AS3CF_Error::log( 'Failed to unserialize offload meta for attachment ' . $attachment->ID . ': ' . $attachment->provider_object ); + AS3CF_Error::log( 'Failed to unserialize offload meta for attachment ' . $item->ID . ': ' . $item->provider_object ); $this->error_count++; return false; } // Using Media_Library_Item::get_by_source_id falls back to legacy metadata and substitutes in defaults and potentially missing values. - $as3cf_item = Media_Library_Item::get_by_source_id( $attachment->ID ); + $as3cf_item = Media_Library_Item::get_by_source_id( $item->ID ); if ( ! $as3cf_item ) { - AS3CF_Error::log( 'Could not construct item for attachment with ID ' . $attachment->ID . ' from legacy offload metadata.' ); + AS3CF_Error::log( 'Could not construct item for attachment with ID ' . $item->ID . ' from legacy offload metadata.' ); $this->error_count++; return false; @@ -76,10 +76,10 @@ protected function upgrade_item( $attachment ) { // Update legacy amazonS3_info record with region required for subsequent upgrades. $provider_object['region'] = $as3cf_item->region(); - $result = update_post_meta( $attachment->ID, 'amazonS3_info', $provider_object ); + $result = update_post_meta( $item->ID, 'amazonS3_info', $provider_object ); if ( false === $result ) { - AS3CF_Error::log( 'Error updating region in legacy offload metadata for attachment ' . $attachment->ID ); + AS3CF_Error::log( 'Error updating region in legacy offload metadata for attachment ' . $item->ID ); $this->error_count++; return false; diff --git a/classes/upgrades/upgrade-tools-errors.php b/classes/upgrades/upgrade-tools-errors.php new file mode 100644 index 00000000..bccd6719 --- /dev/null +++ b/classes/upgrades/upgrade-tools-errors.php @@ -0,0 +1,105 @@ +get_all_tools(); + + return array_keys( $tools ); + } + + /** + * Update saved errors for a tool. + * + * @param mixed $item + * + * @return bool + */ + protected function upgrade_item( $item ) { + global $as3cf; + + if ( empty( $item ) || ! is_string( $item ) ) { + // We really don't want to this upgrade to fail, + // broken notices can still be dismissed, so just move on. + return true; + } + + $sidebar_presenter = Sidebar_Presenter::get_instance( $as3cf ); + $tools = $sidebar_presenter->get_all_tools(); + + if ( ! empty( $tools[ $item ] ) ) { + $tool = $tools[ $item ]; + + $errors = $tool->get_errors(); + $new_errors = array(); + + if ( ! empty( $errors ) ) { + foreach ( $errors as $blog_id => $blog ) { + foreach ( $blog as $attachment_id => $messages ) { + $new_errors[] = (object) array( + 'blog_id' => $blog_id, + 'source_type' => 'media-library', + 'source_id' => $attachment_id, + 'messages' => (array) $messages, + ); + } + } + + $tool->update_errors( $new_errors ); + } + } + + return true; + } +} diff --git a/classes/upgrades/upgrade.php b/classes/upgrades/upgrade.php index 18d7b1cc..279b9921 100644 --- a/classes/upgrades/upgrade.php +++ b/classes/upgrades/upgrade.php @@ -21,7 +21,7 @@ /** * Upgrade Class * - * This class handles updates to attachments and attachment meta data + * This class handles updates to offloaded items. * * @since 0.6.2 */ @@ -113,7 +113,7 @@ abstract class Upgrade { protected $items_processed; /** - * @var mixed Last attachment processed. + * @var mixed Last item processed. */ protected $last_item; @@ -143,8 +143,8 @@ abstract class Upgrade { protected $session; const STATUS_RUNNING = 1; - const STATUS_ERROR = 2; - const STATUS_PAUSED = 3; + const STATUS_ERROR = 2; + const STATUS_PAUSED = 3; /** * Start it up @@ -244,13 +244,13 @@ protected function count_items_to_process() { abstract protected function get_items_to_process( $prefix, $limit, $offset = false ); /** - * Upgrade attachment. + * Upgrade item. * - * @param mixed $attachment + * @param mixed $item * * @return bool */ - abstract protected function upgrade_item( $attachment ); + abstract protected function upgrade_item( $item ); /** * Get running update text. @@ -500,6 +500,7 @@ protected function get_progress_text() { * Calculate progress. * * @return bool|float + * @throws Batch_Limits_Exceeded_Exception */ protected function calculate_progress() { $this->boot_session(); @@ -510,9 +511,9 @@ protected function calculate_progress() { } else { // Set up any per-site state $this->switch_to_blog( get_current_blog_id() ); - $counts = Media_Library_Item::count_attachments(); + $counts = Media_Library_Item::count_items(); - // If there are no attachments, disable progress calculation + // If there are no items, disable progress calculation // and protect against division by zero. if ( ! $counts['total'] ) { return false; diff --git a/include/functions.php b/include/functions.php index d62aabd8..05a44b67 100644 --- a/include/functions.php +++ b/include/functions.php @@ -1,4 +1,7 @@ get_attachment_url( $post_id, null, $size, null, array(), $skip_rewrite_check ); + return as3cf_get_secure_attachment_url( $post_id, null, $size, $skip_rewrite_check ); } } if ( ! function_exists( 'as3cf_get_secure_attachment_url' ) ) { @@ -32,9 +32,11 @@ function as3cf_get_attachment_url( $post_id, $size = null, $skip_rewrite_check = * @return string|bool|WP_Error */ function as3cf_get_secure_attachment_url( $post_id, $expires = 900, $size = null, $skip_rewrite_check = false ) { - /** @var Amazon_S3_And_CloudFront $as3cf */ - global $as3cf; + $as3cf_item = Media_Library_Item::get_by_source_id( $post_id ); + if ( ! empty( $as3cf_item ) && ! is_wp_error( $as3cf_item ) && $as3cf_item->served_by_provider( $skip_rewrite_check ) ) { + return $as3cf_item->get_provider_url( $size, $expires ); + } - return $as3cf->get_secure_attachment_url( $post_id, $expires, $size, array(), $skip_rewrite_check ); + return false; } } \ No newline at end of file diff --git a/languages/amazon-s3-and-cloudfront-en.pot b/languages/amazon-s3-and-cloudfront-en.pot index bc453e84..c6f5ea20 100644 --- a/languages/amazon-s3-and-cloudfront-en.pot +++ b/languages/amazon-s3-and-cloudfront-en.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: amazon-s3-and-cloudfront\n" "Report-Msgid-Bugs-To: nom@deliciousbrains.com\n" -"POT-Creation-Date: 2021-07-19 13:57+0100\n" +"POT-Creation-Date: 2022-03-09 13:52+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,17 +17,17 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: classes/amazon-s3-and-cloudfront.php:166 -#: classes/amazon-s3-and-cloudfront.php:167 +#: classes/amazon-s3-and-cloudfront.php:195 +#: classes/amazon-s3-and-cloudfront.php:196 msgid "Offload Media Lite" msgstr "" -#: classes/amazon-s3-and-cloudfront.php:402 -#: classes/amazon-s3-and-cloudfront.php:419 +#: classes/amazon-s3-and-cloudfront.php:436 +#: classes/amazon-s3-and-cloudfront.php:453 msgid "Unknown" msgstr "" -#: classes/amazon-s3-and-cloudfront.php:491 +#: classes/amazon-s3-and-cloudfront.php:534 #: view/bucket-select.php:87 #: view/delivery-provider-select.php:129 #: view/delivery-provider-select.php:149 @@ -38,223 +38,185 @@ msgstr "" msgid "defined in wp-config.php" msgstr "" -#: classes/amazon-s3-and-cloudfront.php:1422 -#, php-format -msgid "Media Library item ID %d. Provided path is not a string" -msgstr "" - -#: classes/amazon-s3-and-cloudfront.php:1430 -#: classes/items/media-library-item.php:133 -#, php-format -msgid "Media Library item with ID %d has damaged meta data" -msgstr "" - -#: classes/amazon-s3-and-cloudfront.php:1441 -#: classes/items/media-library-item.php:144 -#, php-format -msgid "Media Library item with ID %d does not have a valid file path" -msgstr "" - -#: classes/amazon-s3-and-cloudfront.php:1492 -#: classes/amazon-s3-and-cloudfront.php:1718 -#, php-format -msgid "File %s does not exist" -msgstr "" - -#: classes/amazon-s3-and-cloudfront.php:1522 -#, php-format -msgid "Mime type %s is not allowed" -msgstr "" - -#: classes/amazon-s3-and-cloudfront.php:1533 -msgid "Already offloaded to a different provider" -msgstr "" - -#: classes/amazon-s3-and-cloudfront.php:1641 -#: classes/amazon-s3-and-cloudfront.php:1732 -#, php-format -msgid "Error offloading %s to provider: %s" -msgstr "" - -#: classes/amazon-s3-and-cloudfront.php:2855 +#: classes/amazon-s3-and-cloudfront.php:1521 msgid "This action can only be performed through an admin screen." msgstr "" -#: classes/amazon-s3-and-cloudfront.php:2857 +#: classes/amazon-s3-and-cloudfront.php:1523 msgid "Cheatin’ eh?" msgstr "" -#: classes/amazon-s3-and-cloudfront.php:2859 +#: classes/amazon-s3-and-cloudfront.php:1525 msgid "You do not have sufficient permissions to access this page." msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3183 +#: classes/amazon-s3-and-cloudfront.php:1849 msgid "Error Getting Bucket Region" msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3184 +#: classes/amazon-s3-and-cloudfront.php:1850 #, php-format msgid "There was an error attempting to get the region of the bucket %s: %s" msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3287 +#: classes/amazon-s3-and-cloudfront.php:1953 msgid "" "This is a test file to check if the user has write permission to the bucket. " "Delete me if found." msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3293 +#: classes/amazon-s3-and-cloudfront.php:1959 #, php-format msgid "" "There was an error attempting to check the permissions of the bucket %s: %s" msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3389 +#: classes/amazon-s3-and-cloudfront.php:2077 msgid "Error creating bucket" msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3390 +#: classes/amazon-s3-and-cloudfront.php:2078 msgid "Bucket name too short." msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3391 +#: classes/amazon-s3-and-cloudfront.php:2079 msgid "Bucket name too long." msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3392 +#: classes/amazon-s3-and-cloudfront.php:2080 msgid "" "Invalid character. Bucket names can contain lowercase letters, numbers, " "periods and hyphens." msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3393 +#: classes/amazon-s3-and-cloudfront.php:2081 msgid "Error saving bucket" msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3394 +#: classes/amazon-s3-and-cloudfront.php:2082 msgid "Error fetching buckets" msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3395 +#: classes/amazon-s3-and-cloudfront.php:2083 msgid "Error getting URL preview: " msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3396 +#: classes/amazon-s3-and-cloudfront.php:2084 msgid "The changes you made will be lost if you navigate away from this page" msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3397 +#: classes/amazon-s3-and-cloudfront.php:2085 msgid "Getting diagnostic info..." msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3398 +#: classes/amazon-s3-and-cloudfront.php:2086 msgid "Error getting diagnostic info: " msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3399 +#: classes/amazon-s3-and-cloudfront.php:2087 msgctxt "placeholder for hidden access key, 39 char max" msgid "-- not shown --" msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3401 -#: classes/amazon-s3-and-cloudfront.php:5786 +#: classes/amazon-s3-and-cloudfront.php:2089 +#: classes/amazon-s3-and-cloudfront.php:4210 msgid "Settings saved." msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3523 +#: classes/amazon-s3-and-cloudfront.php:2211 msgid "Cheatin' eh?" msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3596 +#: classes/amazon-s3-and-cloudfront.php:2284 #, php-format msgid "Could not set new Delivery Provider: %s" msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3671 -#: classes/amazon-s3-and-cloudfront.php:3801 +#: classes/amazon-s3-and-cloudfront.php:2359 +#: classes/amazon-s3-and-cloudfront.php:2489 msgid "No bucket name provided." msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3680 +#: classes/amazon-s3-and-cloudfront.php:2368 msgid "Bucket name not valid." msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3693 +#: classes/amazon-s3-and-cloudfront.php:2381 msgid "No region provided." msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3770 +#: classes/amazon-s3-and-cloudfront.php:2458 #, php-format msgctxt "Trying to change public access setting for given provider's bucket." msgid "Can't change Block All Public Access setting for %s buckets." msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3779 +#: classes/amazon-s3-and-cloudfront.php:2467 msgid "No block public access setting provided." msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3792 +#: classes/amazon-s3-and-cloudfront.php:2480 msgid "Storage Provider not configured with access credentials." msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3819 +#: classes/amazon-s3-and-cloudfront.php:2507 msgid "Could not change Block All Public Access status for bucket." msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3836 +#: classes/amazon-s3-and-cloudfront.php:2524 msgid "" "Failed to Enable Block All Public Access — We could " "not enable Block All Public Access. You will need to log in to the AWS " "Console and do it manually." msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3838 +#: classes/amazon-s3-and-cloudfront.php:2526 msgid "" "Failed to Disable Block All Public Access — We could " "not disable Block All Public Access. You will need to log in to the AWS " "Console and do it manually." msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3873 +#: classes/amazon-s3-and-cloudfront.php:2561 #: view/provider-select.php:329 msgctxt "placeholder for hidden secret access key, 39 char max" msgid "-- not shown --" msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3896 +#: classes/amazon-s3-and-cloudfront.php:2584 msgid "Key File not valid JSON." msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3916 -#: classes/amazon-s3-and-cloudfront.php:3930 -#: classes/amazon-s3-and-cloudfront.php:3939 +#: classes/amazon-s3-and-cloudfront.php:2604 +#: classes/amazon-s3-and-cloudfront.php:2618 +#: classes/amazon-s3-and-cloudfront.php:2627 msgctxt "missing form field" msgid " not provided." msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3982 +#: classes/amazon-s3-and-cloudfront.php:2670 msgctxt "Show the media library tab" msgid "Media Library" msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3983 +#: classes/amazon-s3-and-cloudfront.php:2671 msgctxt "Show the addons tab" msgid "Addons" msgstr "" -#: classes/amazon-s3-and-cloudfront.php:3984 +#: classes/amazon-s3-and-cloudfront.php:2672 msgctxt "Show the support tab" msgid "Support" msgstr "" -#: classes/amazon-s3-and-cloudfront.php:4160 +#: classes/amazon-s3-and-cloudfront.php:2849 #, php-format msgid "" "WP Offload Media — The file %s has been given %s " "permissions in the bucket." msgstr "" -#: classes/amazon-s3-and-cloudfront.php:4179 +#: classes/amazon-s3-and-cloudfront.php:2868 msgid "" "WP Offload Media Requirement Missing — Looks like you " "don't have an image manipulation library installed on this server and " @@ -262,7 +224,7 @@ msgid "" "Please setup GD or ImageMagick." msgstr "" -#: classes/amazon-s3-and-cloudfront.php:4203 +#: classes/amazon-s3-and-cloudfront.php:2892 #, php-format msgid "" "Missing Table — One or more required database tables " @@ -270,18 +232,18 @@ msgid "" "details. %s" msgstr "" -#: classes/amazon-s3-and-cloudfront.php:5025 +#: classes/amazon-s3-and-cloudfront.php:3714 #, php-format msgid "" "Define your access keys to enable write access to the " "bucket" msgstr "" -#: classes/amazon-s3-and-cloudfront.php:5032 +#: classes/amazon-s3-and-cloudfront.php:3721 msgid "Quick Start Guide" msgstr "" -#: classes/amazon-s3-and-cloudfront.php:5034 +#: classes/amazon-s3-and-cloudfront.php:3723 #, php-format msgid "" "Looks like we don't have write access to this bucket. It's likely that the " @@ -290,7 +252,7 @@ msgid "" "correctly." msgstr "" -#: classes/amazon-s3-and-cloudfront.php:5036 +#: classes/amazon-s3-and-cloudfront.php:3725 #, php-format msgid "" "Looks like we don't have access to the buckets. It's likely that the user " @@ -298,39 +260,39 @@ msgid "" "Please see our %s for instructions on setting up permissions correctly." msgstr "" -#: classes/amazon-s3-and-cloudfront.php:5204 +#: classes/amazon-s3-and-cloudfront.php:3895 msgid "WP Offload Media Activation" msgstr "" -#: classes/amazon-s3-and-cloudfront.php:5205 +#: classes/amazon-s3-and-cloudfront.php:3896 msgid "" "WP Offload Media Lite and WP Offload Media cannot both be active. We've " "automatically deactivated WP Offload Media Lite." msgstr "" -#: classes/amazon-s3-and-cloudfront.php:5207 +#: classes/amazon-s3-and-cloudfront.php:3898 msgid "WP Offload Media Lite Activation" msgstr "" -#: classes/amazon-s3-and-cloudfront.php:5208 +#: classes/amazon-s3-and-cloudfront.php:3899 msgid "" "WP Offload Media Lite and WP Offload Media cannot both be active. We've " "automatically deactivated WP Offload Media." msgstr "" -#: classes/amazon-s3-and-cloudfront.php:5262 +#: classes/amazon-s3-and-cloudfront.php:3953 msgid "More info »" msgstr "" -#: classes/amazon-s3-and-cloudfront.php:5342 +#: classes/amazon-s3-and-cloudfront.php:4022 msgid "this doc" msgstr "" -#: classes/amazon-s3-and-cloudfront.php:5344 +#: classes/amazon-s3-and-cloudfront.php:4024 msgid "WP Offload Media Feature Removed" msgstr "" -#: classes/amazon-s3-and-cloudfront.php:5345 +#: classes/amazon-s3-and-cloudfront.php:4025 #, php-format msgid "" "You had the \"Always non-SSL\" option selected in your settings, but we've " @@ -341,68 +303,21 @@ msgid "" "to the old behavior." msgstr "" -#: classes/amazon-s3-and-cloudfront.php:5380 -msgid "Offload" -msgstr "" - -#: classes/amazon-s3-and-cloudfront.php:5484 -msgid "No" -msgstr "" - -#: classes/amazon-s3-and-cloudfront.php:5489 -msgctxt "Storage provider key name" -msgid "Storage Provider" -msgstr "" - -#: classes/amazon-s3-and-cloudfront.php:5490 -msgctxt "Storage provider name" -msgid "Storage Provider" -msgstr "" - -#: classes/amazon-s3-and-cloudfront.php:5491 -msgctxt "Bucket name" -msgid "Bucket" -msgstr "" - -#: classes/amazon-s3-and-cloudfront.php:5492 -msgctxt "Path to file in bucket" -msgid "Path" -msgstr "" - -#: classes/amazon-s3-and-cloudfront.php:5493 -msgctxt "Location of bucket" -msgid "Region" -msgstr "" - -#: classes/amazon-s3-and-cloudfront.php:5494 -msgctxt "Access control list of the file in bucket" -msgid "Access" -msgstr "" - -#: classes/amazon-s3-and-cloudfront.php:5495 -msgid "URL" -msgstr "" - -#: classes/amazon-s3-and-cloudfront.php:5496 -msgctxt "Whether or not metadata has been verified" -msgid "Verified" -msgstr "" - -#: classes/amazon-s3-and-cloudfront.php:5749 +#: classes/amazon-s3-and-cloudfront.php:4173 msgid "Assets Pull" msgstr "" -#: classes/amazon-s3-and-cloudfront.php:5750 +#: classes/amazon-s3-and-cloudfront.php:4174 msgid "" "An addon for WP Offload Media to serve your site's JS, CSS, and other " "enqueued assets from Amazon CloudFront or another CDN." msgstr "" -#: classes/amazon-s3-and-cloudfront.php:5754 +#: classes/amazon-s3-and-cloudfront.php:4178 msgid "Feature" msgstr "" -#: classes/amazon-s3-and-cloudfront.php:5800 +#: classes/amazon-s3-and-cloudfront.php:4224 #, php-format msgid "" "Amazon Web Services Plugin No Longer Required — As of " @@ -413,7 +328,7 @@ msgid "" "plugin, it should be safe to deactivate and delete it. %2$s" msgstr "" -#: classes/amazon-s3-and-cloudfront.php:5832 +#: classes/amazon-s3-and-cloudfront.php:4256 #, php-format msgid "" "WP Offload Media Settings Moved — You now define your " @@ -546,34 +461,174 @@ msgstr "" msgid "Settings" msgstr "" -#: classes/as3cf-plugin-compatibility.php:639 +#: classes/as3cf-plugin-compatibility.php:949 +#, php-format +msgid "" +"Warning: This site is using PHP %1$s, in a future update WP " +"Offload Media will require PHP %2$s or later. %3$s" +msgstr "" + +#: classes/integrations/media-library.php:205 +#, php-format +msgid "Can't create item from media library item %d" +msgstr "" + +#: classes/integrations/media-library.php:497 +msgid "Offload" +msgstr "" + +#: classes/integrations/media-library.php:923 +msgid "No" +msgstr "" + +#: classes/integrations/media-library.php:933 +msgctxt "Storage provider key name" +msgid "Storage Provider" +msgstr "" + +#: classes/integrations/media-library.php:934 +msgctxt "Storage provider name" +msgid "Storage Provider" +msgstr "" + +#: classes/integrations/media-library.php:935 +msgctxt "Bucket name" +msgid "Bucket" +msgstr "" + +#: classes/integrations/media-library.php:936 +msgctxt "Path to file in bucket" +msgid "Path" +msgstr "" + +#: classes/integrations/media-library.php:937 +msgctxt "Location of bucket" +msgid "Region" +msgstr "" + +#: classes/integrations/media-library.php:938 +msgctxt "Access control list of the file in bucket" +msgid "Access" +msgstr "" + +#: classes/integrations/media-library.php:939 +msgid "URL" +msgstr "" + +#: classes/integrations/media-library.php:940 +msgctxt "Whether or not metadata has been verified" +msgid "Verified" +msgstr "" + +#: classes/items/download-handler.php:84 +msgid "" +"%1$s with ID %d is offloaded to a different provider than currently " +"configured" +msgstr "" + +#: classes/items/download-handler.php:151 #, php-format msgid "The local directory %s does not exist and could not be created." msgstr "" -#: classes/as3cf-plugin-compatibility.php:640 -#: classes/as3cf-plugin-compatibility.php:652 +#: classes/items/download-handler.php:152 #: classes/upgrades/upgrade-meta-wp-error.php:81 #, php-format msgid "" "There was an error attempting to download the file %s from the bucket: %s" msgstr "" -#: classes/as3cf-plugin-compatibility.php:977 +#: classes/items/download-handler.php:158 #, php-format -msgid "" -"Warning: This site is using PHP %1$s, in a future update WP " -"Offload Media will require PHP %2$s or later. %3$s" +msgid "Error downloading %1$s from bucket: %2$s" msgstr "" -#: classes/items/media-library-item.php:97 +#: classes/items/media-library-item.php:142 msgid "Empty Attachment ID passed to " msgstr "" -#: classes/items/media-library-item.php:106 +#: classes/items/media-library-item.php:157 msgid "Invalid Originator passed to " msgstr "" +#: classes/items/media-library-item.php:170 +#, php-format +msgid "Media Library item with ID %d has damaged meta data" +msgstr "" + +#: classes/items/media-library-item.php:181 +#, php-format +msgid "Media Library item with ID %d does not have a valid file path" +msgstr "" + +#: classes/items/media-library-item.php:553 +msgid "Edit" +msgstr "" + +#: classes/items/remove-provider-handler.php:40 +msgid "Invalid object_keys option provided." +msgstr "" + +#: classes/items/remove-provider-handler.php:44 +msgid "Invalid offloaded_files option provided." +msgstr "" + +#: classes/items/remove-provider-handler.php:48 +msgid "" +"Providing both object_keys and offloaded_files options is not supported." +msgstr "" + +#: classes/items/upload-handler.php:49 +#, php-format +msgid "%s with id %d does not have a valid file path" +msgstr "" + +#: classes/items/upload-handler.php:56 +#, php-format +msgid "%s with id %d. Provided path is not a string" +msgstr "" + +#: classes/items/upload-handler.php:63 +#, php-format +msgid "Primary file %s does not exist" +msgstr "" + +#: classes/items/upload-handler.php:75 +#, php-format +msgid "Mime type %s is not allowed" +msgstr "" + +#: classes/items/upload-handler.php:175 +#, php-format +msgid "Bucket name \"%1$s\" is invalid, using \"%2$s\" instead." +msgstr "" + +#: classes/items/upload-handler.php:190 +msgid "" +"The bucket may not be changed via filters for a previously offloaded item." +msgstr "" + +#: classes/items/upload-handler.php:208 +#, php-format +msgid "" +"The offloaded filename must not be changed, \"%1$s\" has been used instead " +"of \"%2$s\"." +msgstr "" + +#: classes/items/upload-handler.php:216 +msgid "The key may not be changed via filters for a previously offloaded item." +msgstr "" + +#: classes/items/upload-handler.php:287 +#, php-format +msgid "File %s does not exist" +msgstr "" + +#: classes/items/upload-handler.php:308 +#, php-format +msgid "Error offloading %1$s to provider: %2$s" +msgstr "" + #: classes/providers/delivery/another-cdn.php:47 #: classes/providers/delivery/digitalocean-spaces-cdn.php:83 msgid "Fast, No Private Media" @@ -644,7 +699,7 @@ msgid "" "running in the background to update URLs in your post content. %2$s" msgstr "" -#: classes/upgrades/upgrade-edd-replace-urls.php:36 +#: classes/upgrades/upgrade-edd-replace-urls.php:38 msgid "and ensuring that only the local URL exists in EDD post meta." msgstr "" @@ -666,25 +721,29 @@ msgid "" "running in the background to update URLs in your post excerpts. %2$s" msgstr "" -#: classes/upgrades/upgrade-filter-post.php:388 +#: classes/upgrades/upgrade-filter-post.php:391 #, php-format msgid "" "Paused Upgrade
The find & replace to update URLs has " "been paused. %s" msgstr "" -#: classes/upgrades/upgrade-filter-post.php:397 +#: classes/upgrades/upgrade-filter-post.php:400 msgid "See our documentation" msgstr "" -#: classes/upgrades/upgrade-filter-post.php:403 +#: classes/upgrades/upgrade-filter-post.php:406 #, php-format msgid "" "%s for details on why we’re doing this, why it runs slowly, and how to " "make it run faster." msgstr "" -#: classes/upgrades/upgrade-items-table.php:48 +#: classes/upgrades/upgrade-item-extra-data.php:48 +msgid "and updating metadata about offloaded items to new format." +msgstr "" + +#: classes/upgrades/upgrade-items-table.php:47 msgid "" "and updating the plugin's metadata to use a faster storage method. During " "the update the site's total offloaded media count may be inaccurate but will " @@ -703,6 +762,10 @@ msgid "" "style=\"white-space:nowrap;\">(e.g. s3-us-west-2.amazonaws.com)." msgstr "" +#: classes/upgrades/upgrade-tools-errors.php:37 +msgid "and reformatting internal data about previous errors from tools ." +msgstr "" + #: classes/upgrades/upgrade-wpos3-to-as3cf.php:36 msgid "" "and updating the metadata to use key names compatible with the current " @@ -751,12 +814,12 @@ msgstr "" msgid " (%s%% Complete)" msgstr "" -#: classes/upgrades/upgrade.php:629 +#: classes/upgrades/upgrade.php:630 #, php-format msgid "Every %d Minutes" msgstr "" -#: classes/upgrades/upgrade.php:956 +#: classes/upgrades/upgrade.php:957 #, php-format msgid "" "Settings Locked Temporarily — You can't change any of " @@ -787,15 +850,15 @@ msgctxt "Install plugin now" msgid "Install Now" msgstr "" -#: view/attachment-metabox.php:20 +#: view/attachment-metabox.php:27 msgid "This item has not been offloaded yet." msgstr "" -#: view/attachment-metabox.php:56 +#: view/attachment-metabox.php:63 msgid "File does not exist on server" msgstr "" -#: view/attachment-metabox.php:64 +#: view/attachment-metabox.php:71 msgid "File exists on server" msgstr "" diff --git a/readme.txt b/readme.txt index b8863449..4c3631f4 100644 --- a/readme.txt +++ b/readme.txt @@ -2,9 +2,9 @@ Contributors: bradt, deliciousbrains, ianmjones Tags: uploads, amazon, s3, amazon s3, digitalocean, digitalocean spaces, google cloud storage, gcs, mirror, admin, media, cdn, cloudfront Requires at least: 4.9 -Tested up to: 5.8 -Requires PHP: 5.5 -Stable tag: 2.5.5 +Tested up to: 5.9 +Requires PHP: 5.6 +Stable tag: 2.6.0 License: GPLv3 Copies files to Amazon S3, DigitalOcean Spaces or Google Cloud Storage as they are uploaded to the Media Library. Optionally configure Amazon CloudFront or another CDN for faster delivery. @@ -13,7 +13,7 @@ Copies files to Amazon S3, DigitalOcean Spaces or Google Cloud Storage as they a FORMERLY WP OFFLOAD S3 LITE -https://www.youtube.com/watch?v=_PVybEGaRXc +https://www.youtube.com/watch?v=I-wTMXMeFu4 This plugin automatically copies images, videos, documents, and any other media added through WordPress' media uploader to [Amazon S3](http://aws.amazon.com/s3/), [DigitalOcean Spaces](https://www.digitalocean.com/products/spaces/) or [Google Cloud Storage](https://cloud.google.com/storage/). It then automatically replaces the URL to each media file with their respective Amazon S3, DigitalOcean Spaces or Google Cloud Storage URL or, if you have configured [Amazon CloudFront](http://aws.amazon.com/cloudfront/) or another CDN with or without a custom domain, that URL instead. Image thumbnails are also copied to the bucket and delivered through the correct remote URL. @@ -67,6 +67,10 @@ If you upgrade to the pro version of [WP Offload Media](https://deliciousbrains. == Upgrade Notice == += 2.6 = +This is a major upgrade that updates the format of information stored about offloaded Media Library items. Once upgraded you will not be able to downgrade without restoring data from a backup. +This version requires PHP 5.6+ + = 2.3 = This is a major upgrade that switches to using a custom table for storing data about offloaded Media Library items. Once upgraded you will not be able to downgrade without restoring data from a backup. @@ -81,6 +85,16 @@ This version requires PHP 5.3.3+ and the Amazon Web Services plugin == Changelog == += WP Offload Media Lite 2.6 - 2022-03-09 = +* [Release Summary Blog Post](https://deliciousbrains.com/wp-offload-media-2-6-released/?utm_campaign=changelogs&utm_source=wordpress.org&utm_medium=free%2Bplugin%2Blisting) +* New: WP Offload Media is now compatible with WordPress 5.9 and Full Site Editing +* Improvement: Offloaded thumbnail sizes are now tracked for better handling of changes to registered sizes +* Improvement: Offloads and other storage provider actions are faster +* Bug fix: URL rewriting now works in the Full Site Editor +* Bug fix: Offloaded images are now shown when re-editing a Block Template or Template Part +* Bug fix: URL rewriting now works for Widgets migrated to a Widget Sidebar Block +* Bug fix: Objects are no longer left in the bucket when deleting a Media Library item with many changes to its thumbnail sizes + = WP Offload Media Lite 2.5.5 - 2021-07-19 = * Bug fix: Signed GCS URLs broken when updating a post * Bug fix: Incorrect mime type set on scaled image's bucket object when thumbnail format differs from original file's format diff --git a/view/attachment-metabox.php b/view/attachment-metabox.php index bb92e32e..60fad02b 100644 --- a/view/attachment-metabox.php +++ b/view/attachment-metabox.php @@ -1,4 +1,8 @@ get_integration_manager()->get_integration( 'mlib' ); ?>
@@ -21,49 +28,49 @@
-
get_media_action_strings( 'provider' ); ?>:
+
get_media_action_strings( 'provider' ); ?>:
-
get_media_action_strings( 'bucket' ); ?>:
+
get_media_action_strings( 'bucket' ); ?>:
-
get_media_action_strings( 'key' ); ?>:
+
get_media_action_strings( 'key' ); ?>:
-
get_media_action_strings( 'region' ); ?>:
+
get_media_action_strings( 'region' ); ?>:
-
get_media_action_strings( 'acl' ); ?>:
+
get_media_action_strings( 'acl' ); ?>:
- get_acl_value_string( $provider_object['acl'], $post->ID ); ?> + get_acl_value_string( $provider_object['acl'], $post->ID ); ?>
-
get_media_action_strings( 'is_verified' ); ?>:
-
get_media_action_strings( 'not_verified' ); ?>
+
get_media_action_strings( 'is_verified' ); ?>:
+
get_media_action_strings( 'not_verified' ); ?>
@@ -75,15 +82,15 @@
diff --git a/wordpress-s3.php b/wordpress-s3.php index dbb44d75..a882ccb0 100644 --- a/wordpress-s3.php +++ b/wordpress-s3.php @@ -4,8 +4,8 @@ Plugin URI: http://wordpress.org/extend/plugins/amazon-s3-and-cloudfront/ Description: Automatically copies media uploads to Amazon S3, DigitalOcean Spaces or Google Cloud Storage for storage and delivery. Optionally configure Amazon CloudFront or another CDN for even faster delivery. Author: Delicious Brains -Version: 2.5.5 -Author URI: https://deliciousbrains.com/ +Version: 2.6.0 +Author URI: https://deliciousbrains.com/?utm_campaign=WP%2BOffload%2BS3&utm_source=wordpress.org&utm_medium=free%2Bplugin%2Blisting Network: True Text Domain: amazon-s3-and-cloudfront Domain Path: /languages/ @@ -26,7 +26,7 @@ // Then completely rewritten. */ -$GLOBALS['aws_meta']['amazon-s3-and-cloudfront']['version'] = '2.5.5'; +$GLOBALS['aws_meta']['amazon-s3-and-cloudfront']['version'] = '2.6.0'; require_once dirname( __FILE__ ) . '/classes/as3cf-compatibility-check.php';