diff --git a/php/class-media.php b/php/class-media.php index a5f0a7fd8..5740ca4e9 100644 --- a/php/class-media.php +++ b/php/class-media.php @@ -645,34 +645,10 @@ public function get_public_id_from_url( $url, $as_sync_key = false ) { if ( ! $this->is_cloudinary_url( $url ) ) { return null; } - - $path = wp_parse_url( $url, PHP_URL_PATH ); - $parts = explode( '/', ltrim( $path, '/' ) ); - - $maybe_seo = array(); - - // Need to find the version part as anything after this is the public id. - foreach ( $parts as $part ) { - $maybe_seo[] = array_shift( $parts ); // Get rid of the first element. - if ( 'v' === substr( $part, 0, 1 ) && is_numeric( substr( $part, 1 ) ) ) { - break; // Stop removing elements. - } - } - - // The remaining items should be the file. - $file = implode( '/', $parts ); - $path_info = Utils::pathinfo( $file ); - - // Is SEO friendly URL. - if ( in_array( 'images', $maybe_seo, true ) ) { - $public_id = $path_info['dirname']; - } else { - $public_id = isset( $path_info['dirname'] ) && '.' !== $path_info['dirname'] ? $path_info['dirname'] . DIRECTORY_SEPARATOR . $path_info['filename'] : $path_info['filename']; - } - $public_id = trim( $public_id, './' ); - + + $public_id = Utils::parse_url( $url, CLOUDINARY_URL_PUBLIC_ID ); if ( $as_sync_key ) { - $transformations = $this->get_transformations_from_string( $url ); + $transformations = Utils::parse_url( $url, CLOUDINARY_URL_TRANSFORMATIONS_PARSED ); $public_id .= ! empty( $transformations ) ? wp_json_encode( $transformations ) : ''; } @@ -1263,7 +1239,7 @@ public function cloudinary_url( $attachment_id, $size = array(), $transformation // Make a copy as not to destroy the options in \Cloudinary::cloudinary_url(). $args = $pre_args; - $url = $this->plugin->components['connect']->api->cloudinary_url( $cloudinary_id, $args, $set_size ); + $url = $this->plugin->components['connect']->api->cloudinary_url( $cloudinary_id, $args, $set_size, $attachment_id ); // Check if this type is a preview only type. i.e PDF. if ( ! empty( $set_size ) && $this->is_preview_only( $attachment_id ) ) { @@ -1782,10 +1758,13 @@ public function is_cloudinary_url( $url ) { if ( ! filter_var( utf8_uri_encode( $url ), FILTER_VALIDATE_URL ) ) { return false; } - $test_parts = wp_parse_url( $url ); - $cld_url = $this->plugin->components['connect']->api->asset_url; + $test_parts = wp_parse_url( $url ); + $cloudinary_hosts = array( + $this->plugin->components['connect']->api->asset_url, + Utils::CLOUDINARY_HOST, + ); - return isset( $test_parts['path'] ) && $test_parts['host'] === $cld_url; + return isset( $test_parts['path'] ) && in_array( $test_parts['host'], $cloudinary_hosts, true ); } /** diff --git a/php/class-plugin.php b/php/class-plugin.php index 248fd5b3a..25b280fe9 100644 --- a/php/class-plugin.php +++ b/php/class-plugin.php @@ -585,6 +585,77 @@ protected function setup_endpoints() { if ( ! defined( 'CLOUDINARY_ENDPOINTS_VIDEO_PLAYER_VERSION' ) ) { define( 'CLOUDINARY_ENDPOINTS_VIDEO_PLAYER_VERSION', '1.5.1' ); } + + /** + * The Cloudinary URL public ID. + */ + if ( ! defined( 'CLOUDINARY_URL_PUBLIC_ID' ) ) { + define( 'CLOUDINARY_URL_PUBLIC_ID', 8 ); + } + + /** + * The Cloudinary URL version. + */ + if ( ! defined( 'CLOUDINARY_URL_VERSION' ) ) { + define( 'CLOUDINARY_URL_VERSION', 9 ); + } + + /** + * The Cloudinary URL transformations. + */ + if ( ! defined( 'CLOUDINARY_URL_TRANSFORMATIONS' ) ) { + define( 'CLOUDINARY_URL_TRANSFORMATIONS', 10 ); + } + + /** + * The Cloudinary URL parsed transformations. + */ + if ( ! defined( 'CLOUDINARY_URL_TRANSFORMATIONS_PARSED' ) ) { + define( 'CLOUDINARY_URL_TRANSFORMATIONS_PARSED', 11 ); + } + + /** + * The Cloudinary URL asset type. + */ + if ( ! defined( 'CLOUDINARY_URL_ASSET_TYPE' ) ) { + define( 'CLOUDINARY_URL_ASSET_TYPE', 12 ); + } + + /** + * The Cloudinary URL delivery. + */ + if ( ! defined( 'CLOUDINARY_URL_DELIVERY' ) ) { + define( 'CLOUDINARY_URL_DELIVERY', 13 ); + } + + /** + * The Cloudinary URL format. + */ + if ( ! defined( 'CLOUDINARY_URL_FORMAT' ) ) { + define( 'CLOUDINARY_URL_FORMAT', 14 ); + } + + /** + * The Cloudinary URL query parsed. + */ + if ( ! defined( 'CLOUDINARY_URL_QUERY_PARSED' ) ) { + define( 'CLOUDINARY_URL_QUERY_PARSED', 15 ); + } + + /** + * The WordPress attachment ID. + */ + if ( ! defined( 'CLOUDINARY_URL_ATTACHMENT_ID' ) ) { + define( 'CLOUDINARY_URL_ATTACHMENT_ID', 16 ); + } + + /** + * The WordPress attachment object. + */ + if ( ! defined( 'CLOUDINARY_URL_ATTACHMENT' ) ) { + define( 'CLOUDINARY_URL_ATTACHMENT', 17 ); + } + } /** diff --git a/php/class-utils.php b/php/class-utils.php index 6877d613c..e1932727c 100644 --- a/php/class-utils.php +++ b/php/class-utils.php @@ -7,6 +7,7 @@ namespace Cloudinary; +use Cloudinary\Connect\Api; use Cloudinary\Settings\Setting; use Google\Web_Stories\Story_Post_Type; @@ -17,6 +18,11 @@ */ class Utils { + /** + * Holds the default/public Cloudinary host. + */ + const CLOUDINARY_HOST = 'res.cloudinary.com'; + /** * Filter an array recursively * @@ -432,4 +438,184 @@ public static function pathinfo( $path, $flags = 15 ) { return is_array( $pathinfo ) ? array_map( 'urldecode', $pathinfo ) : urldecode( $pathinfo ); } + + /** + * Check if a delivery type is Cloudinary supported. + * + * @param string $type The type to check. + * + * @return bool + */ + public static function is_delivery_type( $type ) { + $types = array( + 'upload', + 'private', + 'authenticated', + 'list', + 'fetch', + 'facebook', + 'twitter', + 'twitter_name', + 'gravatar', + 'youtube', + 'hulu', + 'vimeo', + 'animoto', + 'worldstarhiphop', + 'dailymotion', + 'multi ', + 'text', + 'sprite', + ); + + return in_array( $type, $types, true ); + } + + /** + * Parse a Cloudinary URL into components. + * + * @param string $url The URL to parse. + * @param int $component The flag of a single component to get. + * + * @return array|mixed|object|null + */ + public static function parse_url( $url, $component = - 1 ) { + + static $api, $media, $ext_types, $urls, $globals; + + $defaults = array( + 'scheme' => null, + 'host' => null, + 'port' => null, + 'user' => null, + 'pass' => null, + 'path' => null, + 'query' => null, + 'fragment' => null, + 'public_id' => null, + 'version' => 1, + 'transformations' => null, + 'transformations_parsed' => array(), + 'asset_type' => null, + 'delivery' => null, + 'format' => null, + 'query_parsed' => array(), + 'attachment_id' => 0, + 'attachment' => null, + ); + + if ( ! isset( $urls[ $url ] ) ) { + if ( ! $api ) { + $media = get_plugin_instance()->get_component( 'media' ); + $connect = get_plugin_instance()->get_component( 'connect' ); + $api = $connect->api; + $types = wp_get_ext_types(); + $ext_types = array_merge( $types['image'], $types['audio'], $types['video'] ); + $globals = array( + 'image' => $media->apply_default_transformations( array(), 'image' ), + 'video' => $media->apply_default_transformations( array(), 'video' ), + ); + } + $is_seo = false; + $components = wp_parse_args( wp_parse_url( $url ), $defaults ); + + if ( ! $api || ! $media->is_cloudinary_url( $url ) ) { + // Not connected. Return as per normal. + return $components; + } + $parts = explode( '/', trim( $components['path'], '/' ) ); + + /** + * Allocate the parts according to the Cloudinary URL structure. + * + * @see https://cloudinary.com/documentation/image_transformations#transformation_url_structure + */ + if ( self::CLOUDINARY_HOST === $components['host'] ) { + array_shift( $parts ); + } + + // Type and Delivery. + $components['asset_type'] = array_shift( $parts ); + if ( 'images' === $components['asset_type'] ) { + $components['asset_type'] = 'image'; + $components['delivery'] = 'upload'; + $is_seo = true; + } else { + $maybe_delivery_type = array_shift( $parts ); + if ( self::is_delivery_type( $maybe_delivery_type ) ) { + $components['delivery'] = $maybe_delivery_type; + } + } + + // If we don't have a delivery type at this point, the URL is not a proper constructed Cloudinary URL. + if ( $components['delivery'] ) { + // Transformations. + $has_transformations = $media->get_transformations_from_string( ltrim( $components['path'], '/' ), $components['asset_type'] ); + // Remove transformations string if we have any. + if ( ! empty( $has_transformations ) ) { + $type_global = $globals[ $components['asset_type'] ]; + $transformations = Api::generate_transformation_string( $has_transformations, $components['asset_type'] ); + $transformation_parts = explode( '/', $transformations ); + $new_parts = array_diff( $parts, $transformation_parts ); + $parts = array_values( $new_parts ); // Reset the keys since array_diff keeps the indexes. + $type_global['size'] = $media->get_crop_from_transformation( $has_transformations ); + $has_transformations = array_filter( + $has_transformations, + function ( $item ) use ( $type_global ) { + + return ! in_array( $item, $type_global, true ); + } + ); + if ( ! empty( $has_transformations ) ) { + $components['transformations'] = Api::generate_transformation_string( $has_transformations, $components['asset_type'] ); + $components['transformations_parsed'] = $has_transformations; + } + } + + // Version. + if ( 'v' === substr( $parts[0], 0, 1 ) && is_numeric( substr( $parts[0], 1 ) ) ) { + $components['version'] = array_shift( $parts ); + } + + // Get public_id. + $cloudinary_id = implode( '/', $parts ); // Cloudinary ID includes the extension. + $components['public_id'] = $cloudinary_id; + $ext_pos = strrpos( $cloudinary_id, '.' ); + if ( $ext_pos ) { + $format_maybe = substr( $cloudinary_id, $ext_pos + 1 ); + if ( in_array( $format_maybe, $ext_types, true ) ) { + $components['format'] = $format_maybe; + $components['public_id'] = substr( $cloudinary_id, 0, $ext_pos ); + } + } + if ( true === $is_seo ) { + // Check if we have a wp-image-{id}-. + if ( preg_match( '/:wp-image-(\d+):/', '/' . basename( $components['public_id'] ), $match ) ) { + $components['attachment_id'] = intval( $match[1] ); + } + $components['public_id'] = dirname( $components['public_id'] ); + } + } + + if ( ! empty( $components['query'] ) ) { + wp_parse_str( $components['query'], $components['query_parsed'] ); + } + $urls[ $url ] = array_filter( $components ); + } + + $components = $urls[ $url ]; + + // Return the single component if one is specified. + if ( 0 <= $component ) { + $keys = array_keys( $defaults ); + $key = $keys[ $component ]; + $components = isset( $urls[ $url ][ $key ] ) ? $urls[ $url ][ $key ] : null; + // Get the post if requested. + if ( 17 === $component && isset( $urls[ $url ]['attachment_id'] ) ) { + $components = get_post( $urls[ $url ]['attachment_id'] ); + } + } + + return $components; + } } diff --git a/php/connect/class-api.php b/php/connect/class-api.php index c09750544..258cb6a7d 100644 --- a/php/connect/class-api.php +++ b/php/connect/class-api.php @@ -31,7 +31,7 @@ class Api { * * @var string */ - public $asset_url = 'res.cloudinary.com'; + public $asset_url; /** * Cloudinary API Version. @@ -146,6 +146,7 @@ class Api { public function __construct( $connect, $version ) { $this->credentials = $connect->get_credentials(); $this->plugin_version = $version; + $this->asset_url = Utils::CLOUDINARY_HOST; // Use CNAME. if ( ! empty( $this->credentials['cname'] ) ) { $this->asset_url = $this->credentials['cname']; @@ -241,13 +242,16 @@ function ( $item ) use ( $transformation_index ) { /** * Generate a Cloudinary URL. * - * @param string|null $public_id The Public ID to get a url for. - * @param array $args Additional args. - * @param array $size The WP Size array. + * @since 3.0.4 The $attachment_id param is added. + * + * @param string|null $public_id The Public ID to get a url for. + * @param array $args Additional args. + * @param array $size The WP Size array. + * @param null|int $attachment_id The attachment ID if present. * * @return string */ - public function cloudinary_url( $public_id = null, $args = array(), $size = array() ) { + public function cloudinary_url( $public_id = null, $args = array(), $size = array(), $attachment_id = null ) { if ( null === $public_id ) { return 'https://' . $this->url( null, null ); @@ -274,7 +278,11 @@ public function cloudinary_url( $public_id = null, $args = array(), $size = arra $base = Utils::pathinfo( $public_id ); // Only do dynamic naming and sizes if upload type. if ( 'image' === $args['resource_type'] && 'upload' === $args['delivery_type'] ) { - $new_path = $base['filename'] . '/' . $base['basename']; + $basename = $base['basename']; + if ( ! empty( $attachment_id ) ) { + $basename = ':wp-image-' . $attachment_id . ':' . $basename; + } + $new_path = $base['filename'] . '/' . $basename; $public_id = str_replace( $base['basename'], $new_path, $public_id ); } @@ -493,7 +501,7 @@ public function upload( $attachment_id, $args, $headers = array(), $try_remote = /** * Filter Cloudinary upload args. * - * @hook cloudinary_upload_args + * @hook cloudinary_upload_args * @since 3.0.1 * * @param $call_args {array} The default args.