diff --git a/php/class-assets.php b/php/class-assets.php index 9315bbe5b..6ece25e45 100644 --- a/php/class-assets.php +++ b/php/class-assets.php @@ -355,24 +355,6 @@ public function add_url_replacements() { wp_safe_redirect( $referrer ); exit; } - if ( ! empty( $this->delivery->known ) ) { - - foreach ( $this->delivery->known as $url => $set ) { - if ( is_int( $set ) || empty( $set['public_id'] ) ) { - continue; - } - $public_id = $set['public_id']; - if ( ! empty( $set['format'] ) ) { - $public_id .= '.' . $set['format']; - } - $cloudinary_url = $this->media->cloudinary_url( $set['post_id'], array( $set['width'], $set['height'] ), null, $public_id ); - if ( $cloudinary_url ) { - // Late replace on unmatched urls (links, inline styles etc..), both http and https. - String_Replace::replace( 'http:' . $url, $cloudinary_url ); - String_Replace::replace( 'https:' . $url, $cloudinary_url ); - } - } - } } /** diff --git a/php/class-delivery.php b/php/class-delivery.php index 7ad07395b..8a0e87f3d 100644 --- a/php/class-delivery.php +++ b/php/class-delivery.php @@ -201,7 +201,7 @@ public function filter_out_cloudinary( $content ) { $transformations = $this->media->get_transformations_from_string( $original_url ); if ( 'image' === $this->media->get_resource_type( $result['post_id'] ) ) { - $attachment_url = wp_get_attachment_image_url( $result['post_id'], $size ); + $attachment_url = wp_get_attachment_image_url( $result['post_id'], $size ); } else { $attachment_url = wp_get_attachment_url( $result['post_id'] ); } @@ -567,7 +567,7 @@ protected function setup_hooks() { // Add filters. add_filter( 'content_save_pre', array( $this, 'filter_out_cloudinary' ) ); add_action( 'save_post', array( $this, 'remove_replace_cache' ) ); - add_action( 'cloudinary_string_replace', array( $this, 'catch_urls' ) ); + add_action( 'cloudinary_string_replace', array( $this, 'catch_urls' ), 10, 2 ); add_filter( 'post_thumbnail_html', array( $this, 'process_featured_image' ), 100, 3 ); add_filter( 'cloudinary_current_post_id', array( $this, 'get_current_post_id' ) ); @@ -669,6 +669,12 @@ public function setup() { */ protected function init_delivery() { + // Reset internals. + $this->known = array(); + $this->unknown = array(); + $this->found_urls = array(); + $this->unusable = array(); + add_filter( 'wp_calculate_image_srcset', array( $this->media, 'image_srcset' ), 10, 5 ); /** @@ -807,7 +813,6 @@ protected function get_context_cache() { */ public function get_media_tags( $content, $tags = 'img|video' ) { $images = array(); - $urls = ''; if ( preg_match_all( '#(?P<(' . $tags . ')[^>]*\>){1}#is', $content, $found ) ) { $count = count( $found[0] ); for ( $i = 0; $i < $count; $i ++ ) { @@ -822,16 +827,17 @@ public function get_media_tags( $content, $tags = 'img|video' ) { * Convert media tags from Local to Cloudinary, and register with String_Replace. * * @param string $content The HTML to find tags and prep replacement in. + * @param string $context The content of the content. * * @return array */ - public function convert_tags( $content ) { + public function convert_tags( $content, $context = 'view' ) { $has_cache = $this->get_context_cache(); $type = is_ssl() ? 'https' : 'http'; if ( Utils::is_amp() ) { $type = 'amp'; } - if ( ! empty( $has_cache[ $type ] ) ) { + if ( 'view' === $context && ! empty( $has_cache[ $type ] ) ) { $cached = $has_cache[ $type ]; } @@ -863,29 +869,50 @@ public function convert_tags( $content ) { continue; } $this->current_post_id = $set['context']; - // Use cached item if found. - if ( isset( $cached[ $set['original'] ] ) ) { - $replacements[ $set['original'] ] = $cached[ $set['original'] ]; - } else { - // Register replacement. - $replacements[ $set['original'] ] = $this->rebuild_tag( $set ); + + // We only rebuild tags in the view context. + if ( 'view' === $context ) { + // Use cached item if found. + if ( isset( $cached[ $set['original'] ] ) ) { + $replacements[ $set['original'] ] = $cached[ $set['original'] ]; + } else { + // Register replacement. + $replacements[ $set['original'] ] = $this->rebuild_tag( $set ); + } } $this->current_post_id = null; - // Check for src aliases. - if ( isset( $this->found_urls[ $set['base_url'] ] ) ) { - $base = dirname( $set['base_url'] ); - foreach ( $this->found_urls[ $set['base_url'] ] as $size => $file_name ) { - $local_url = $type . ':' . path_join( $base, $file_name ); - if ( isset( $cached[ $local_url ] ) ) { - $aliases[ $local_url ] = $cached[ $local_url ]; - continue; - } - $cloudinary_url = $this->media->cloudinary_url( $set['id'], explode( 'x', $size ), $set['transformations'], $set['atts']['data-public-id'], $set['overwrite_transformations'] ); - $aliases[ $local_url ] = $cloudinary_url; + } + + // Create aliases for urls where were found, but not found with an ID in a tag. + // Create the Full/Scaled items first. + foreach ( $this->known as $url => $relation ) { + if ( $url === $relation['public_id'] ) { + continue; // We don't need the public_id relation item. + } + $base = $type . ':' . $url; + $public_id = ! is_admin() ? $relation['public_id'] . '.' . $relation['format'] : null; + $aliases[ $base ] = $this->media->cloudinary_url( $relation['post_id'], array(), $relation['transformations'], $public_id ); + } + + // Create the sized found relations second. + foreach ( $this->found_urls as $url => $sizes ) { + if ( ! isset( $this->known[ $url ] ) ) { + continue; + } + $base = $type . ':' . $url; + $relation = $this->known[ $url ]; + $public_id = ! is_admin() ? $relation['public_id'] . '.' . $relation['format'] : null; + foreach ( $sizes as $size => $file_name ) { + $local_url = path_join( dirname( $base ), $file_name ); + if ( isset( $cached[ $local_url ] ) ) { + $aliases[ $local_url ] = $cached[ $local_url ]; + continue; } + $aliases[ $local_url ] = $this->media->cloudinary_url( $relation['post_id'], explode( 'x', $size ), $relation['transformations'], $public_id ); } } + // Move aliases to the end of the run, after images. if ( ! empty( $aliases ) ) { $replacements = array_merge( $replacements, $aliases ); @@ -1526,11 +1553,15 @@ public function prepare_delivery( $content ) { $urls = array_merge( $desized, $scaled ); $urls = array_values( $urls ); // resets the index. - // clean out empty urls. - $cloudinary_urls = array_filter( $base_urls, array( $this->media, 'is_cloudinary_url' ) ); // clean out empty urls. - // Clean URLS for search. - $public_ids = array_filter( array_map( array( $this->media, 'get_public_id_from_url' ), $cloudinary_urls ) ); - + $public_ids = array(); + // Lets only look for Cloudinary URLs on the frontend. + if ( ! is_admin() ) { + // clean out empty urls. + $cloudinary_urls = array_filter( $base_urls, array( $this->media, 'is_cloudinary_url' ) ); // clean out empty urls. + // Clean URLS for search. + $all_public_ids = array_filter( array_map( array( $this->media, 'get_public_id_from_url' ), $cloudinary_urls ) ); + $public_ids = array_unique( $all_public_ids ); + } if ( empty( $urls ) && empty( $public_ids ) ) { return; // Bail since theres nothing. } @@ -1586,22 +1617,24 @@ public function query_relations( $public_ids, $urls = array() ) { * Catch attachment URLS from HTML content. * * @param string $content The HTML to catch URLS from. + * @param string $context The content of the content. */ - public function catch_urls( $content ) { + public function catch_urls( $content, $context = 'view' ) { $this->init_delivery(); $this->prepare_delivery( $content ); - $known = $this->convert_tags( $content ); + if ( ! empty( $this->known ) ) { + $known = $this->convert_tags( $content, $context ); + // Replace the knowns. + foreach ( $known as $src => $replace ) { + String_Replace::replace( $src, $replace ); + } + } // Attempt to get the unknowns. if ( ! empty( $this->unknown ) ) { $this->find_attachment_size_urls(); } - // Replace the knowns. - foreach ( $known as $src => $replace ) { - String_Replace::replace( $src, $replace ); - } - } } diff --git a/php/class-media.php b/php/class-media.php index d20469dd9..58247abbb 100644 --- a/php/class-media.php +++ b/php/class-media.php @@ -1225,7 +1225,7 @@ protected function get_cache_key( $args ) { * * @param int $attachment_id The id of the attachment. * @param array|string $size The wp size to set for the URL. - * @param array $transformations Set of transformations to apply to this url. + * @param array|string $transformations Set of transformations to apply to this url. * @param string|null $cloudinary_id Optional forced cloudinary ID. * @param bool $overwrite_transformations Flag url is a breakpoint URL to stop re-applying default transformations. * @@ -1260,6 +1260,9 @@ public function cloudinary_url( $attachment_id, $size = array(), $transformation $set_size = $this->prepare_size( $attachment_id, $size ); } // Prepare transformations. + if ( ! empty( $transformations ) && is_string( $transformations ) ) { + $transformations = $this->get_transformations_from_string( $transformations, $resource_type ); + } $pre_args['transformation'] = $this->get_transformations( $attachment_id, $transformations, $overwrite_transformations ); // Make a copy as not to destroy the options in \Cloudinary::cloudinary_url(). @@ -2772,6 +2775,8 @@ public function apply_media_library_filters( $query ) { if ( ! empty( $result ) ) { $query->set( 'post__in', $result ); + } else { + $query->set( 'post__in', array( 0 ) ); } } } @@ -2838,7 +2843,6 @@ public function setup() { add_action( 'print_media_templates', array( $this, 'media_template' ) ); add_action( 'wp_enqueue_media', array( $this, 'editor_assets' ) ); add_action( 'wp_ajax_cloudinary-down-sync', array( $this, 'down_sync_asset' ) ); - add_action( 'rest_api_init', array( $this, 'add_live_url_filters' ) ); // Filter to add cloudinary folder. add_filter( 'upload_dir', array( $this, 'upload_dir' ) ); diff --git a/php/class-string-replace.php b/php/class-string-replace.php index ad87c415c..64656bb26 100644 --- a/php/class-string-replace.php +++ b/php/class-string-replace.php @@ -8,6 +8,7 @@ namespace Cloudinary; use Cloudinary\Component\Setup; +use \Traversable; /** * String replace class. @@ -21,6 +22,13 @@ class String_Replace implements Setup { */ public $plugin; + /** + * Holds the context. + * + * @var string + */ + protected $context; + /** * Holds the list of strings and replacements. * @@ -41,10 +49,38 @@ public function __construct( Plugin $plugin ) { * Setup the object. */ public function setup() { - add_action( 'the_content', array( $this, 'replace_strings' ), 1 ); - add_action( 'template_redirect', array( $this, 'init' ), -1000 ); // Not crazy low, but low enough to catch most cases, but not too low that it may break AMP. + if ( is_admin() ) { + $this->admin_filters(); + } else { + $this->public_filters(); + } + $this->add_rest_filters(); + } + + /** + * Add admin filters. + */ + protected function admin_filters() { + // Admin filters can call String_Replace frequently, which is fine, as performance is not an issue. + add_filter( 'media_send_to_editor', array( $this, 'replace_strings' ), 10, 2 ); + add_filter( 'the_editor_content', array( $this, 'replace_strings' ), 10, 2 ); + add_filter( 'wp_prepare_attachment_for_js', array( $this, 'replace_strings' ), 11 ); + add_action( 'admin_init', array( $this, 'start_capture' ) ); + } + + /** + * Add Public Filters. + */ + protected function public_filters() { add_action( 'template_include', array( $this, 'init_debug' ), PHP_INT_MAX ); - $types = get_post_types_by_support( 'editor' ); + add_action( 'parse_request', array( $this, 'init' ), - 1000 ); // Not crazy low, but low enough to catch most cases, but not too low that it may break AMP. + } + + /** + * Add filters for REST API. + */ + protected function add_rest_filters() { + $types = get_post_types_by_support( 'author' ); foreach ( $types as $type ) { $post_type = get_post_type_object( $type ); // Check if this is a rest supported type. @@ -66,9 +102,9 @@ public function setup() { */ public function pre_filter_rest_content( $response, $post, $request ) { $context = $request->get_param( 'context' ); - if ( 'view' === $context ) { - $data = $response->get_data(); - $data['content']['rendered'] = $this->replace_strings( $data['content']['rendered'] ); + if ( 'view' === $context || 'edit' === $context ) { + $data = $response->get_data(); + $data = $this->replace_strings( $data, $context ); $response->set_data( $data ); } @@ -79,12 +115,20 @@ public function pre_filter_rest_content( $response, $post, $request ) { * Init the buffer capture and set the output callback. */ public function init() { - remove_action( 'the_content', array( $this, 'replace_strings' ), 1 ); // Remove the content filter. if ( ! defined( 'CLD_DEBUG' ) || false === CLD_DEBUG ) { - ob_start( array( $this, 'replace_strings' ) ); + $this->context = 'view'; + $this->start_capture(); } } + /** + * Stop the buffer capture and set the output callback. + */ + public function start_capture() { + ob_start( array( $this, 'replace_strings' ) ); + ob_start( array( $this, 'replace_strings' ) ); // Second call to catch early buffer flushing. + } + /** * Init the buffer capture in debug mode. * @@ -94,6 +138,7 @@ public function init() { */ public function init_debug( $template ) { if ( defined( 'CLD_DEBUG' ) && true === CLD_DEBUG && ! Utils::get_sanitized_text( '_bypass' ) ) { + $this->context = 'view'; ob_start(); include $template; $html = ob_get_clean(); @@ -137,44 +182,114 @@ public static function replace( $search, $replace ) { } /** - * Replace string in HTML. + * Flatten an array into content. * - * @param string $html The HTML. + * @param array $content The array to flatten. * * @return string */ - public function replace_strings( $html ) { + public function flatten( $content ) { + $flat = ''; + if ( self::is_iterable( $content ) ) { + foreach ( $content as $item ) { + $flat .= "\r\n" . $this->flatten( $item ); + } + } else { + $flat = $content; + } + + return $flat; + } + + /** + * Check if the item is iterable. + * + * @param mixed $thing Thing to check. + * + * @return bool + */ + public static function is_iterable( $thing ) { + + return is_array( $thing ) || is_object( $thing ); + } + /** + * Prime replacement strings. + * + * @param mixed $content The content to prime replacements for. + * @param string $context The context to use. + */ + protected function prime_replacements( $content, $context = 'view' ) { + + if ( self::is_iterable( $content ) ) { + $content = $this->flatten( $content ); + } /** * Do replacement action. * - * @hook cloudinary_string_replace + * @hook cloudinary_string_replace + * @since 3.0.3 Added the `$context` argument. * - * @param $html {string} The html of the page. + * @param $content {string} The html of the page. + * @param $context {string} The render context. */ - do_action( 'cloudinary_string_replace', $html ); + do_action( 'cloudinary_string_replace', $content, $context ); + } + + /** + * Replace string in HTML. + * + * @param string|array $content The HTML. + * @param string $context The context to use. + * + * @return string + */ + public function replace_strings( $content, $context = 'view' ) { + static $last_content; + if ( empty( $content ) || $last_content === $content ) { + return $content; // Bail if nothing to replace. + } + // Captured a front end request, since the $context will be an int. + if ( ! empty( $this->context ) ) { + $context = $this->context; + } + if ( Utils::looks_like_json( $content ) ) { + $json_maybe = json_decode( $content, true ); + if ( $json_maybe ) { + $content = $json_maybe; + } + } + $this->prime_replacements( $content, $context ); if ( ! empty( self::$replacements ) ) { - $html = self::do_replace( $html ); + $content = self::do_replace( $content ); } - - return $html; + self::reset(); + $last_content = isset( $json_maybe ) ? wp_json_encode( $content ) : $content; + return $last_content; } /** * Do string replacements. * - * @param string $content The content to do replacements on. + * @param string|array $content The content to do replacements on. * - * @return string + * @return string|array */ public static function do_replace( $content ) { - return str_replace( array_keys( self::$replacements ), array_values( self::$replacements ), $content ); + + if ( self::is_iterable( $content ) ) { + foreach ( $content as &$item ) { + $item = self::do_replace( $item ); + } + } else { + $content = str_replace( array_keys( self::$replacements ), array_values( self::$replacements ), $content ); + } + + return $content; } /** * Reset internal replacements. - * - * @return void */ public static function reset() { self::$replacements = array(); diff --git a/php/class-utils.php b/php/class-utils.php index 0384a9f78..33205dc3a 100644 --- a/php/class-utils.php +++ b/php/class-utils.php @@ -445,4 +445,15 @@ public static function pathinfo( $path, $flags = 15 ) { return is_array( $pathinfo ) ? array_map( 'urldecode', $pathinfo ) : urldecode( $pathinfo ); } + + /** + * Check if a thing looks like a json string. + * + * @param mixed $thing The thing to check. + * + * @return bool + */ + public static function looks_like_json( $thing ) { + return is_string( $thing ) && in_array( ltrim( $thing )[0], array( '{', '[' ), true ); + } } diff --git a/php/media/class-filter.php b/php/media/class-filter.php index 9d33cac8d..98303aae0 100644 --- a/php/media/class-filter.php +++ b/php/media/class-filter.php @@ -573,9 +573,6 @@ public function pre_filter_rest_content( $response, $post, $request ) { $context = $request->get_param( 'context' ); if ( 'edit' === $context ) { $data = $response->get_data(); - $content = $data['content']['raw']; - $data['content']['raw'] = $this->filter_out_local( $content ); - // Handle meta if missing due to custom-fields not being supported. if ( ! isset( $data['meta'] ) ) { $data['meta'] = $request->get_param( 'meta' ); @@ -769,14 +766,6 @@ public function setup_hooks() { add_filter( 'wp_insert_post_data', array( $this, 'prepare_amp_posts' ), 11 ); add_filter( 'wp_prepare_attachment_for_js', array( $this, 'filter_attachment_for_js' ), 11 ); - // Add support for custom header. - add_filter( 'get_header_image_tag', array( $this, 'filter_out_local' ) ); - - // Add transformations. - add_filter( 'media_send_to_editor', array( $this, 'transform_to_editor' ), 10, 3 ); - - // Filter video codes. - add_filter( 'media_send_to_editor', array( $this, 'filter_video_embeds' ), 10, 3 ); // Enable Rest filters. add_action( 'rest_api_init', array( $this, 'init_rest_filters' ) ); @@ -790,13 +779,6 @@ public function setup_hooks() { // Filter to record current meta updating attachment. add_filter( 'wp_update_attachment_metadata', array( $this, 'record_meta_update' ), 10, 2 ); - // Filter out locals and responsive images setup. - if ( is_admin() ) { - // Filtering out locals. - add_filter( 'the_editor_content', array( $this, 'filter_out_local' ) ); - add_filter( 'the_content', array( $this, 'filter_out_local' ), 100 ); - } - // Add filter to match src when editing in block. add_filter( 'wp_image_file_matches_image_meta', array( $this, 'edit_match_src' ), 10, 4 ); } diff --git a/php/sync/class-sync-queue.php b/php/sync/class-sync-queue.php index 07d290b9d..3ba013d53 100644 --- a/php/sync/class-sync-queue.php +++ b/php/sync/class-sync-queue.php @@ -320,9 +320,11 @@ protected function query_unsynced_data() { $cached = get_transient( Sync::META_KEYS['dashboard_cache'] ); if ( empty( $cached ) ) { + $return = array(); $wpdb->cld_table = Utils::get_relationship_table(); - $return['total_assets'] = (int) $wpdb->get_var( "SELECT COUNT( DISTINCT post_id ) as total FROM {$wpdb->cld_table};" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery - $return['unoptimized_assets'] = (int) $wpdb->get_var( "SELECT COUNT( DISTINCT post_id ) as total FROM {$wpdb->cld_table} WHERE public_id IS NULL;" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $query_string = "SELECT COUNT( DISTINCT post_id ) as total FROM {$wpdb->cld_table} JOIN {$wpdb->posts} on( {$wpdb->cld_table}.post_id = {$wpdb->posts}.ID ) WHERE {$wpdb->posts}.post_type = 'attachment'"; + $return['total_assets'] = (int) $wpdb->get_var( $query_string ); // phpcs:ignore WordPress.DB + $return['unoptimized_assets'] = (int) $wpdb->get_var( $query_string . ' AND public_id IS NULL' ); // phpcs:ignore WordPress.DB $asset_sizes = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->prepare(