diff --git a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-media.php b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-media.php index 2ffd89d18..004d4fb07 100644 --- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-media.php +++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-media.php @@ -655,7 +655,11 @@ public function get_public_id( $attachment_id ) { public function get_cloudinary_id( $attachment_id ) { // A cloudinary_id is a public_id with a file extension. - $public_id = $this->get_public_id( $attachment_id ); + $public_id = $this->get_public_id( $attachment_id ); + $suffix_data = $this->get_post_meta( $attachment_id, Sync::META_KEYS['suffix'], true ); + if ( is_array( $suffix_data ) && ! empty( $suffix_data['suffix'] ) && $suffix_data['public_id'] === $public_id ) { + $public_id = $public_id . $suffix_data['suffix']; + } $file = get_attached_file( $attachment_id ); $info = pathinfo( $file ); $cloudinary_id = $public_id . '.' . $info['extension']; diff --git a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-sync.php b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-sync.php index a33f2208f..d0b4067ce 100644 --- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-sync.php +++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/class-sync.php @@ -49,6 +49,7 @@ class Sync implements Setup, Assets { 'sync_error' => '_sync_error', 'cloudinary' => '_cloudinary_v2', 'folder_sync' => '_folder_sync', + 'suffix' => '_suffix', 'syncing' => '_cloudinary_syncing', 'downloading' => '_cloudinary_downloading', ); @@ -172,6 +173,77 @@ public function get_signature( $post_id ) { return $return; } + /** + * Generate a new Public ID for an asset. + * + * @param int $attachment_id The attachment ID for the new public ID. + * + * @return string|null + */ + public function generate_public_id( $attachment_id ) { + $settings = $this->plugin->config['settings']; + $cld_folder = trailingslashit( $settings['sync_media']['cloudinary_folder'] ); + $file = get_attached_file( $attachment_id ); + $file_info = pathinfo( $file ); + $public_id = $cld_folder . $file_info['filename']; + + return $public_id; + } + + /** + * Maybe add a suffix to the public ID if it's not unique. + * + * @param string $public_id The public ID to maybe add a suffix. + * @param int $attachment_id The attachment ID. + * @param string|null $suffix The suffix to maybe add. + * + * @return string The public ID. + */ + public function add_suffix_maybe( $public_id, $attachment_id, $suffix = null ) { + + // Test if asset exists by calling just the head on the asset url, to prevent API rate limits. + $url = $this->plugin->components['connect']->api->cloudinary_url( $public_id . $suffix ); + $req = wp_remote_head( $url, array( 'body' => array( 'rdm' => wp_rand( 100, 999 ) ) ) ); + $asset_error = strtolower( wp_remote_retrieve_header( $req, 'x-cld-error' ) ); + $code = wp_remote_retrieve_response_code( $req ); + + // If the request is not a 404 & does not have a cld-error header stating resource not found, it exists and should be checked that it's not a resync or generate a prefixed ID. + if ( 404 !== $code && false === strpos( $asset_error, 'resource not found' ) ) { + + // Get the attachment type. + if ( wp_attachment_is( 'image', $attachment_id ) ) { + $type = 'image'; + } elseif ( wp_attachment_is( 'video', $attachment_id ) ) { + $type = 'video'; + } elseif ( wp_attachment_is( 'audio', $attachment_id ) ) { + $type = 'audio'; + } else { + // not supported. + return null; + } + $cld_asset = $this->plugin->components['connect']->api->get_asset_details( $public_id, $type ); + if ( ! is_wp_error( $cld_asset ) && ! empty( $cld_asset['public_id'] ) ) { + $context_id = null; + + // Exists, check to see if this asset originally belongs to this ID. + if ( ! empty( $cld_asset['context'] ) && ! empty( $cld_asset['context']['custom'] ) && ! empty( $cld_asset['context']['custom']['wp_id'] ) ) { + $context_id = (int) $cld_asset['context']['custom']['wp_id']; + } + + // Generate new ID only if context ID is not related. + if ( $context_id !== $attachment_id ) { + // Generate a new ID with a uniqueID prefix. + $suffix = '-' . uniqid(); + + // Return new potential suffixed ID. + return $this->add_suffix_maybe( $public_id, $attachment_id, $suffix ); + } + } + } + + return $suffix; + } + /** * Additional component setup. */ @@ -179,6 +251,7 @@ public function setup() { if ( $this->plugin->config['connect'] ) { $this->managers['upload']->setup(); $this->managers['delete']->setup(); + $this->managers['push']->setup(); } } } diff --git a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/connect/class-api.php b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/connect/class-api.php index 279a7da93..08d80d6ec 100644 --- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/connect/class-api.php +++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/connect/class-api.php @@ -269,6 +269,20 @@ public function cloudinary_url( $public_id, $args = array(), $size = array(), $c return implode( '/', $url_parts ); } + /** + * Get the details of an asset by public ID. + * + * @param string $public_id The public_id to check. + * @param string $type The asset type. + * + * @return array|\WP_Error + */ + public function get_asset_details( $public_id, $type ) { + $url = $this->url( 'resources', $type . '/upload/' . $public_id, true ); + + return $this->call( $url, array( 'body' => $args ), 'get' ); + } + /** * Upload a large asset in chunks. * diff --git a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/sync/class-push-sync.php b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/sync/class-push-sync.php index 09ef6f25d..e95bc6e31 100644 --- a/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/sync/class-push-sync.php +++ b/cloudinary-image-management-and-manipulation-in-the-cloud-cdn/php/sync/class-push-sync.php @@ -47,6 +47,34 @@ class Push_Sync { */ protected $post_id; + /** + * Holds the media component. + * + * @var \Cloudinary\Media + */ + protected $media; + + /** + * Holds the sync component. + * + * @var \Cloudinary\Sync\ + */ + protected $sync; + + /** + * Holds the connect component. + * + * @var \Cloudinary\Connect + */ + protected $connect; + + /** + * Holds the Rest_API component. + * + * @var \Cloudinary\REST_API + */ + protected $api; + /** * Push_Sync constructor. * @@ -76,6 +104,17 @@ private function register_hooks() { add_filter( 'cloudinary_api_rest_endpoints', array( $this, 'rest_endpoints' ) ); } + /** + * Setup this component. + */ + public function setup() { + // Setup components. + $this->media = $this->plugin->components['media']; + $this->sync = $this->plugin->components['sync']; + $this->connect = $this->plugin->components['connect']; + $this->api = $this->plugin->components['api']; + } + /** * Add endpoints to the \Cloudinary\REST_API::$endpoints array. * @@ -143,7 +182,7 @@ public function rest_get_queue_status() { return rest_ensure_response( array( 'success' => true, - 'data' => $this->plugin->components['sync']->managers['queue']->get_queue_status(), + 'data' => $this->sync->managers['queue']->get_queue_status(), ) ); } @@ -158,13 +197,13 @@ public function rest_get_queue_status() { public function rest_start_sync( \WP_REST_Request $request ) { $stop = $request->get_param( 'stop' ); - $queue = $this->plugin->components['sync']->managers['queue']->get_queue(); + $queue = $this->sync->managers['queue']->get_queue(); if ( empty( $queue['pending'] ) || ! empty( $stop ) ) { - $this->plugin->components['sync']->managers['queue']->stop_queue(); + $this->sync->managers['queue']->stop_queue(); return $this->rest_get_queue_status(); // Nothing to sync. } - $this->plugin->components['sync']->managers['queue']->start_queue(); + $this->sync->managers['queue']->start_queue(); return $this->call_thread(); @@ -202,18 +241,18 @@ public function rest_push_attachments( \WP_REST_Request $request ) { // Process queue based. if ( ! empty( $last_id ) && ! empty( $last_result ) ) { - $this->plugin->components['sync']->managers['queue']->mark( $last_id, $last_result ); + $this->sync->managers['queue']->mark( $last_id, $last_result ); } - if ( ! $this->plugin->components['sync']->managers['queue']->is_running() ) { // Check it wasn't stopped. + if ( ! $this->sync->managers['queue']->is_running() ) { // Check it wasn't stopped. return $this->rest_get_queue_status(); } - $this->post_id = $this->plugin->components['sync']->managers['queue']->get_post(); + $this->post_id = $this->sync->managers['queue']->get_post(); // No post, end of queue. if ( empty( $this->post_id ) ) { - $this->plugin->components['sync']->managers['queue']->stop_queue(); + $this->sync->managers['queue']->stop_queue(); return $this->rest_get_queue_status(); } @@ -233,7 +272,7 @@ public function resume_queue() { add_filter( 'cloudinary_on_demand_sync_enabled', '__return_false' ); // Disable the on-demand sync since we want the status. define( 'DOING_BULK_SYNC', true ); // Define bulk sync in action. - if ( ! $this->plugin->components['sync']->is_synced( $this->post_id ) ) { + if ( ! $this->sync->is_synced( $this->post_id ) ) { $stat = $this->push_attachments( array( $this->post_id ) ); if ( ! empty( $stat['processed'] ) ) { $result = 'done'; @@ -269,7 +308,7 @@ private function call_thread( $last_id = null, $last_result = null ) { } // Setup background call to continue the queue. - $this->plugin->components['api']->background_request( 'process', $params ); + $this->api->background_request( 'process', $params ); return $this->rest_get_queue_status(); } @@ -304,7 +343,7 @@ private function get_sync_type( $attachment ) { $type = 'upload'; // Check for explicit (has public_id, but no breakpoints). - $attachment_signature = $this->plugin->components['sync']->get_signature( $attachment->ID ); + $attachment_signature = $this->sync->get_signature( $attachment->ID ); if ( empty( $attachment_signature ) ) { if ( ! empty( $attachment->{Sync::META_KEYS['public_id']} ) ) { // Has a public id but no signature, explicit update to complete download. @@ -313,7 +352,7 @@ private function get_sync_type( $attachment ) { // fallback to upload. } else { // Has signature find differences and use specific sync method. - $required_signature = $this->plugin->components['sync']->generate_signature( $attachment->ID ); + $required_signature = $this->sync->generate_signature( $attachment->ID ); foreach ( $required_signature as $key => $signature ) { if ( ( ! isset( $attachment_signature[ $key ] ) || $attachment_signature[ $key ] !== $signature ) && isset( $this->sync_types[ $key ] ) ) { return $this->sync_types[ $key ]; @@ -331,11 +370,11 @@ private function get_sync_type( $attachment ) { * Prepare an attachment for upload. * * @param int|\WP_Post $post The attachment to prepare. - * @param bool $down_sync Flag to determine if a missing file starts a downsync. + * @param bool $push_sync Flag to determine if this prep is for a sync. True = build for syncing, false = build for validation. * * @return array|\WP_Error */ - public function prepare_upload( $post, $down_sync = false ) { + public function prepare_upload( $post, $push_sync = false ) { if ( is_numeric( $post ) ) { $post = get_post( $post ); @@ -351,9 +390,6 @@ public function prepare_upload( $post, $down_sync = false ) { return new \WP_Error( 'attachment_post_expected', __( 'An attachment post was expected.', 'cloudinary' ) ); } - // Get the media component. - $media = $this->plugin->components['media']; - // First check if this has a file and it can be uploaded. $file = get_attached_file( $post->ID ); $file_size = 0; @@ -363,10 +399,10 @@ public function prepare_upload( $post, $down_sync = false ) { } elseif ( ! file_exists( $file ) ) { // May be an old upload type. $src = get_post_meta( $post->ID, '_wp_attached_file', true ); - if ( $media->is_cloudinary_url( $src ) ) { + if ( $this->media->is_cloudinary_url( $src ) ) { // Download first maybe. - if ( true === $down_sync ) { - $download = $this->plugin->components['sync']->managers['download']->down_sync( $post->ID ); + if ( true === $push_sync ) { + $download = $this->sync->managers['download']->down_sync( $post->ID ); if ( is_wp_error( $download ) ) { update_post_meta( $post->ID, Sync::META_KEYS['sync_error'], $download->get_error_message() ); @@ -384,12 +420,12 @@ public function prepare_upload( $post, $down_sync = false ) { $resource_type = $this->get_resource_type( $post ); $max_size = ( 'image' === $resource_type ? 'max_image_size' : 'max_video_size' ); - if ( ! empty( $this->plugin->components['connect']->usage[ $max_size ] ) && $file_size > $this->plugin->components['connect']->usage[ $max_size ] ) { - $max_size_hr = size_format( $this->plugin->components['connect']->usage[ $max_size ] ); + if ( ! empty( $this->connect->usage[ $max_size ] ) && $file_size > $this->connect->usage[ $max_size ] ) { + $max_size_hr = size_format( $this->connect->usage[ $max_size ] ); // translators: variable is file size. $error = sprintf( __( 'File size exceeds the maximum of %s. This media asset will be served from WordPress.', 'cloudinary' ), $max_size_hr ); - $media->delete_post_meta( $post->ID, Sync::META_KEYS['pending'] ); // Remove Flag. + $this->media->delete_post_meta( $post->ID, Sync::META_KEYS['pending'] ); // Remove Flag. // Cleanup flags delete_post_meta( $post->ID, Sync::META_KEYS['syncing'] ); @@ -403,8 +439,8 @@ public function prepare_upload( $post, $down_sync = false ) { $public_id = $post->{Sync::META_KEYS['public_id']}; // use the __get method on the \WP_Post to get post_meta. $cld_folder = trailingslashit( $settings['sync_media']['cloudinary_folder'] ); if ( empty( $public_id ) ) { - $file_info = pathinfo( $file ); - $public_id = $cld_folder . $file_info['filename']; + // Create a new public_id. + $public_id = $this->sync->generate_public_id( $post->ID ); } // Assume that the public_id is a root item. @@ -419,7 +455,7 @@ public function prepare_upload( $post, $down_sync = false ) { $public_id_file = $public_id_info['filename']; } // Check if this asset is a folder sync. - $folder_sync = $media->get_post_meta( $post->ID, Sync::META_KEYS['folder_sync'], true ); + $folder_sync = $this->media->get_post_meta( $post->ID, Sync::META_KEYS['folder_sync'], true ); if ( ! empty( $folder_sync ) && false === $downsync ) { $public_id_folder = $cld_folder; // Ensure the public ID folder is constant. } else { @@ -452,7 +488,7 @@ public function prepare_upload( $post, $down_sync = false ) { $imagesize = getimagesize( $file ); $meta['width'] = $imagesize[0]; } - $max_width = $media->get_max_width(); + $max_width = $this->media->get_max_width(); // Add breakpoints request options. if ( ! empty( $settings['global_transformations']['enable_breakpoints'] ) ) { $options['responsive_breakpoints'] = array( @@ -462,7 +498,7 @@ public function prepare_upload( $post, $down_sync = false ) { 'max_width' => $meta['width'] < $max_width ? $meta['width'] : $max_width, 'min_width' => $settings['global_transformations']['min_width'], ); - $transformations = $media->get_transformation_from_meta( $post->ID ); + $transformations = $this->media->get_transformation_from_meta( $post->ID ); if ( ! empty( $transformations ) ) { $options['responsive_breakpoints']['transformation'] = Api::generate_transformation_string( $transformations ); } @@ -493,11 +529,39 @@ public function prepare_upload( $post, $down_sync = false ) { } // Restructure the path to the filename to allow correct placement in Cloudinary. - $public_id = ltrim( $public_id_folder . $options['public_id'], '/' ); + $public_id = ltrim( $public_id_folder . $options['public_id'], '/' ); + // If this is a push sync, make sure the ID is allowed and unique. + + // Setup suffix data for unique ids. + $suffix_defaults = array( + 'public_id' => $public_id, + 'suffix' => null, + ); + $suffix_meta = $this->media->get_post_meta( $post->ID, Sync::META_KEYS['suffix'], true ); + $suffix_data = wp_parse_args( $suffix_meta, $suffix_defaults ); + + // Prepare a uniqueness check and get a suffix if needed. + if ( true === $push_sync ) { + if ( $public_id !== $suffix_data['public_id'] || empty( $suffix_data['suffix'] ) ) { + $suffix_data['suffix'] = $this->sync->add_suffix_maybe( $public_id, $post->ID ); + if ( ! empty( $suffix_data['suffix'] ) ) { + // Only save if there is a suffix to save on metadata. + $this->media->update_post_meta( $post->ID, Sync::META_KEYS['suffix'], $suffix_data ); + } else { + // Clear meta data in case of a unique name on a rename etc. + $this->media->delete_post_meta( $post->ID, Sync::META_KEYS['suffix'] ); + } + } + } + // Add Suffix to public_id. + if ( ! empty( $suffix_data['suffix'] ) ) { + $public_id = $public_id . $suffix_data['suffix']; + } $return = array( 'file' => $file, 'folder' => ltrim( $cld_folder, '/' ), 'public_id' => $public_id, + 'suffix' => $suffix_data['suffix'], 'breakpoints' => array(), 'options' => $options, ); @@ -542,8 +606,6 @@ public function push_attachments( $attachments ) { 'total' => count( $attachments ), 'processed' => 0, ); - // Get media component. - $media = $this->plugin->components['media']; // Go over each attachment. foreach ( $attachments as $attachment ) { @@ -597,48 +659,48 @@ public function push_attachments( $attachments ) { if ( ! empty( $upload['options']['context'] ) ) { $args['context'] = $upload['options']['context']; } - $result = $this->plugin->components['connect']->api->explicit( $args ); + $result = $this->connect->api->explicit( $args ); } elseif ( 'rename' === $sync_type ) { // Rename an asset. $args = array( - 'from_public_id' => $media->get_post_meta( $attachment->ID, Sync::META_KEYS['public_id'] ), + 'from_public_id' => $this->media->get_post_meta( $attachment->ID, Sync::META_KEYS['public_id'] ), 'to_public_id' => $upload['public_id'], ); - $result = $this->plugin->components['connect']->api->{$upload['options']['resource_type']}( 'rename', 'POST', $args ); + $result = $this->connect->api->{$upload['options']['resource_type']}( 'rename', 'POST', $args ); } else { // dynamic sync type.. - $result = $this->plugin->components['connect']->api->{$sync_type}( $upload['file'], $upload['options'] ); + $result = $this->connect->api->{$sync_type}( $upload['file'], $upload['options'] ); } } else { // Large Upload. - $result = $this->plugin->components['connect']->api->upload_large( $upload['file'], $upload['options'] ); + $result = $this->connect->api->upload_large( $upload['file'], $upload['options'] ); } // Exceptions are handled by the Upload wrapper class and returned as \WP_Error, so check for it. if ( is_wp_error( $result ) ) { $error = $result->get_error_message(); $stats['fail'][] = $error; - $media->update_post_meta( $attachment->ID, Sync::META_KEYS['sync_error'], $error ); + $this->media->update_post_meta( $attachment->ID, Sync::META_KEYS['sync_error'], $error ); continue; } // Successful upload, so lets update meta. if ( is_array( $result ) && ( array_key_exists( 'public_id', $result ) || array_key_exists( 'public_ids', $result ) ) ) { $meta_data = array( - Sync::META_KEYS['signature'] => $this->plugin->components['sync']->generate_signature( $attachment->ID ), + Sync::META_KEYS['signature'] => $this->sync->generate_signature( $attachment->ID ), ); // We only have a version if updating a file or an upload. if ( ! empty( $result['version'] ) ) { $meta_data[ Sync::META_KEYS['version'] ] = $result['version']; } - $media->delete_post_meta( $attachment->ID, Sync::META_KEYS['pending'] ); + $this->media->delete_post_meta( $attachment->ID, Sync::META_KEYS['pending'] ); // Cleanup flags delete_post_meta( $attachment->ID, Sync::META_KEYS['downloading'] ); delete_post_meta( $attachment->ID, Sync::META_KEYS['syncing'] ); - $media->delete_post_meta( $attachment->ID, Sync::META_KEYS['sync_error'], false ); + $this->media->delete_post_meta( $attachment->ID, Sync::META_KEYS['sync_error'], false ); if ( ! empty( $this->plugin->config['settings']['global_transformations']['enable_breakpoints'] ) ) { if ( ! empty( $result['responsive_breakpoints'] ) ) { // Images only. $meta_data[ Sync::META_KEYS['breakpoints'] ] = $result['responsive_breakpoints'][0]['breakpoints']; @@ -647,6 +709,12 @@ public function push_attachments( $attachments ) { delete_post_meta( $attachment->ID, Sync::META_KEYS['breakpoints'] ); } } + + // Reset public_id without suffix. + if ( ! empty( $upload['suffix'] ) ) { + $upload['options']['public_id'] = strstr( $upload['options']['public_id'], $upload['suffix'], true ); + } + // Generate a public_id sync hash. if ( ! empty( $upload['options']['public_id'] ) ) { // a transformation breakpoints only ever happens on a down sync. $sync_key = '_' . md5( $upload['options']['public_id'] ); @@ -659,7 +727,8 @@ public function push_attachments( $attachments ) { $meta = wp_get_attachment_metadata( $attachment->ID, true ); $meta[ Sync::META_KEYS['cloudinary'] ] = $meta_data; wp_update_attachment_metadata( $attachment->ID, $meta ); - $media->update_post_meta( $attachment->ID, Sync::META_KEYS['public_id'], $upload['options']['public_id'] ); + + $this->media->update_post_meta( $attachment->ID, Sync::META_KEYS['public_id'], $upload['options']['public_id'] ); // Search and update link references in content. $content_search = new \WP_Query( array( 's' => 'wp-image-' . $attachment->ID, 'fields' => 'ids', 'posts_per_page' => 1000 ) ); if ( ! empty( $content_search->found_posts ) ) {