diff --git a/.wordpress-org/blueprints/blueprint.json b/.wordpress-org/blueprints/blueprint.json new file mode 100644 index 0000000..5cb8cf9 --- /dev/null +++ b/.wordpress-org/blueprints/blueprint.json @@ -0,0 +1,30 @@ +{ + "landingPage": "\/wp-admin\/plugins.php", + "preferredVersions": { + "php": "8.0", + "wp": "latest" + }, + "phpExtensionBundles": [ + "kitchen-sink" + ], + "features": { + "networking": true + }, + "steps": [ + { + "step": "installPlugin", + "pluginZipFile": { + "resource": "url", + "url": "https:\/\/downloads.wordpress.org\/plugin\/cf-images.zip" + }, + "options": { + "activate": true + } + }, + { + "step": "login", + "username": "admin", + "password": "password" + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 05b3508..6705b8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ += 1.9.2 - 17.07.2024 = + +Added: +* Integration with WPBakery page builder image galleries +* Integration with Elementor Pro Gallery +* Integration with Flatsome theme gallery +* cf_images_upload_host filter to adjust the image host ID + +Changed: +* Improve image AI modules +* Improve performance when Rank Math image SEO is active + +Fixed: +* Only allow generating image alt text for supported formats (JPEG, PNG, GIF, BMP) +* Duplicate queries for images that are not part of the media library +* Rank Math image SEO module not working with custom domains + = 1.9.1 - 23.04.2024 = Added: diff --git a/README.md b/README.md index cfb3162..94ea19e 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ === Offload, AI & Optimize with Cloudflare Images === Plugin Name: Offload, AI & Optimize with Cloudflare Images Contributors: vanyukov -Tags: cdn, cloudflare images, image ai, compress, optimize +Tags: cdn, cloudflare images, image AI, compress, optimize Donate link: https://www.paypal.com/donate/?business=JRR6QPRGTZ46N&no_recurring=0&item_name=Help+support+the+development+of+the+Cloudflare+Images+plugin+for+WordPress¤cy_code=AUD Requires at least: 5.6 Requires PHP: 7.0 -Tested up to: 6.5 -Stable tag: 1.9.1 +Tested up to: 6.6 +Stable tag: 1.9.2 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -102,6 +102,23 @@ If something is still not working for you, please let me know by creating a supp == Changelog == += 1.9.2 - 17.07.2024 = + +Added: +* Integration with WPBakery page builder image galleries +* Integration with Elementor Pro Gallery +* Integration with Flatsome theme gallery +* cf_images_upload_host filter to adjust the image host ID + +Changed: +* Improve image AI modules +* Improve performance when Rank Math image SEO is active + +Fixed: +* Only allow generating image alt text for supported formats (JPEG, PNG, GIF, BMP) +* Duplicate queries for images that are not part of the media library +* Rank Math image SEO module not working with custom domains + = 1.9.1 - 23.04.2024 = Added: diff --git a/app/class-core.php b/app/class-core.php index 9c0ef4e..48be090 100644 --- a/app/class-core.php +++ b/app/class-core.php @@ -178,6 +178,8 @@ private function set_cdn_domain() { * @see Integrations\Spectra * @see Integrations\Wpml * @see Integrations\Shortpixel + * @see Integrations\JS_Composer + * @see Integrations\Flatsome */ private function init_integrations() { $loader = Loader::get_instance(); @@ -189,6 +191,8 @@ private function init_integrations() { $loader->integration( 'wpml' ); $loader->integration( 'shortpixel' ); $loader->integration( 'elementor' ); + $loader->integration( 'js-composer' ); + $loader->integration( 'flatsome' ); } /** diff --git a/app/class-image.php b/app/class-image.php index 860548e..488d7ee 100644 --- a/app/class-image.php +++ b/app/class-image.php @@ -439,7 +439,7 @@ private function get_crop_string( int $width, array $size ): string { private function attachment_url_to_post_id( string $url ): int { $post_id = wp_cache_get( $url, 'cf_images' ); - if ( ! $post_id ) { + if ( false === $post_id ) { global $wpdb; $sql = $wpdb->prepare( @@ -452,16 +452,17 @@ private function attachment_url_to_post_id( string $url ): int { if ( $results ) { $post_id = reset( $results )->ID; - wp_cache_add( $url, $post_id, 'cf_images' ); } else { // This is a fallback, in case the above doesn't work for some reason. $results = attachment_url_to_postid( $url ); if ( $results ) { $post_id = $results; - wp_cache_add( $url, $post_id, 'cf_images' ); } } + + // Store this regardless if we have the post ID, prevents duplicate queries. + wp_cache_add( $url, $post_id, 'cf_images' ); } return $post_id; diff --git a/app/class-media.php b/app/class-media.php index 86385e7..a132f1d 100644 --- a/app/class-media.php +++ b/app/class-media.php @@ -421,7 +421,17 @@ public function upload_image( $metadata, int $attachment_id, string $action = '' $host = $url['host']; } - $name = trailingslashit( $host ) . str_replace( trailingslashit( $dir['basedir'] ), '', $path ); + /** + * This filters allows modifying the host slug in the image path that is used to identify the image on Cloudflare. + * + * @since 1.9.2 + * + * @param string $host Site domain. + * @param int $attachment_id Attachment ID. + */ + $host = apply_filters( 'cf_images_upload_host', $host, $attachment_id ); + + $name = ( $host ? trailingslashit( $host ) : '' ) . str_replace( trailingslashit( $dir['basedir'] ), '', $path ); try { // This allows us to replace the image on Cloudflare. diff --git a/app/integrations/class-elementor.php b/app/integrations/class-elementor.php index 9883555..5c889ad 100644 --- a/app/integrations/class-elementor.php +++ b/app/integrations/class-elementor.php @@ -17,6 +17,7 @@ use CF_Images\App\Traits; use Elementor\Widget_Base; use Elementor\Widget_Image_Carousel; +use ElementorPro\Modules\Gallery\Widgets\Gallery; if ( ! defined( 'WPINC' ) ) { die; @@ -53,17 +54,18 @@ public function __construct() { * @param Widget_Base $widget The widget. */ public function add_lightbox_support( string $widget_content, Widget_Base $widget ): string { - if ( ! $widget instanceof Widget_Image_Carousel ) { + if ( ! $widget instanceof Widget_Image_Carousel && ! $widget instanceof Gallery ) { return $widget_content; } // Regular expression to find tags with data-elementor-open-lightbox="yes" and Cloudflare Images links. - $pattern = '/(]*data-elementor-open-lightbox="yes"[^>]*href=")(' . preg_quote( $this->get_cdn_domain(), '/' ) . '[^"]+)(")/i'; + $pattern = '/(]*href="' . preg_quote( $this->get_cdn_domain(), '/' ) . '[^"#]*)(#[^"]*)?(".*?data-elementor-open-lightbox="yes".*?>)/i'; // Callback function to append '#.jpg' to the href attribute. $callback = function ( $matches ) { - // Append '#.jpg' only if it's not already appended. - return $matches[1] . $matches[2] . ( substr( $matches[2], -5 ) !== '#.jpg' ? '#.jpg' : '' ) . $matches[3]; + // Check if '#.jpg' is not already appended, and append if necessary. + $new_url = $matches[1] . ( isset( $matches[2] ) && strpos( $matches[2], '#.jpg' ) !== false ? $matches[2] : '#.jpg' ); + return $new_url . $matches[3]; }; return preg_replace_callback( $pattern, $callback, $widget_content ); diff --git a/app/integrations/class-flatsome.php b/app/integrations/class-flatsome.php new file mode 100644 index 0000000..2c1326f --- /dev/null +++ b/app/integrations/class-flatsome.php @@ -0,0 +1,48 @@ + + * @since 1.9.2 + */ + +namespace CF_Images\App\Integrations; + +use CF_Images\App\Modules\Cloudflare_Images; + +if ( ! defined( 'WPINC' ) ) { + die; +} + +/** + * Flatsome class. + * + * @since 1.9.2 + */ +class Flatsome { + /** + * Class constructor. + * + * @since 1.9.2 + */ + public function __construct() { + add_action( 'wp_ajax_flatsome_additional_variation_images_load_images_ajax_frontend', array( $this, 'load_images_ajax' ) ); + add_action( 'wp_ajax_nopriv_flatsome_additional_variation_images_load_images_ajax_frontend', array( $this, 'load_images_ajax' ) ); + } + + /** + * Add support for additional variation images (Flatsome gallery). + * + * @since 1.9.2 + */ + public function load_images_ajax() { + $cf_images = new Cloudflare_Images( 'cloudflare-images' ); + add_filter( 'wp_get_attachment_image_src', array( $cf_images, 'get_attachment_image_src' ), 10, 3 ); + } +} diff --git a/app/integrations/class-js-composer.php b/app/integrations/class-js-composer.php new file mode 100644 index 0000000..4d9bc89 --- /dev/null +++ b/app/integrations/class-js-composer.php @@ -0,0 +1,94 @@ + + * @since 1.9.2 + */ + +namespace CF_Images\App\Integrations; + +use CF_Images\App\Modules\Cloudflare_Images; +use CF_Images\App\Traits\Helpers; + +if ( ! defined( 'WPINC' ) ) { + die; +} + +/** + * JS_Composer class. + * + * @since 1.9.2 + */ +class JS_Composer { + use Helpers; + + /** + * Class constructor. + * + * @since 1.9.2 + */ + public function __construct() { + add_filter( 'vc_wpb_getimagesize', array( $this, 'fix_getimagesize_paths' ), 10, 3 ); + } + + /** + * When using custom image sizes on gallery images, WPBakery strips out Cloudflare Images parameters, + * breaking the images. This fixes the images, by appending the required image parameters. + * + * @since 1.9.2 + * + * @param array|bool $image_data Array with image data. + * @param string|int $attachment_id Attachment ID. + * @param array $params Image parameters. + * + * @return array + */ + public function fix_getimagesize_paths( $image_data, $attachment_id, array $params ): array { + if ( ! isset( $image_data['thumbnail'] ) || ! isset( $image_data['p_img_large'] ) || ! is_array( $image_data['p_img_large'] ) ) { + return $image_data; + } + + $pattern = '/<(?:img|source)\b(?>\s+(?:src=[\'"]([^\'"]*)[\'"]|srcset=[\'"]([^\'"]*)[\'"])|[^\s>]+|\s+)*>/i'; + if ( ! preg_match_all( $pattern, $image_data['thumbnail'], $images ) ) { + do_action( 'cf_images_log', 'Running fix_getimagesize_paths(), `src` not found, returning image. Attachment ID: %s. Image: %s', $attachment_id, $image_data['thumbnail'] ); + return $image_data; + } + + // Check if the image has the 'w' or 'h' attribute set. + if ( preg_match( '/[?&]([wh])=\d+/', $images[1][0] ) ) { + return $image_data; + } + + // The image is not on Cloudflare, exit. + if ( false === strpos( $image_data['p_img_large'][0], $this->get_cdn_domain() ) ) { + return $image_data; + } + + // Now let's add the correct parameters to the original image. + if ( ! isset( $params['thumb_size'] ) || ! is_string( $params['thumb_size'] ) || ! preg_match( '/(\d+)x(\d+)/', $params['thumb_size'], $size ) ) { + return $image_data; + } + + list( $hash, $cloudflare_image_id ) = Cloudflare_Images::get_hash_id_url_string( (int) $attachment_id ); + + if ( empty( $cloudflare_image_id ) || ( empty( $hash ) && ! apply_filters( 'cf_images_module_enabled', false, 'custom-path' ) ) ) { + return $image_data; + } + + $image_url = trailingslashit( $this->get_cdn_domain() . "/$hash" ) . "$cloudflare_image_id/w=$size[1],h=$size[2]"; + if ( $size[1] === $size[2] ) { + $image_url .= ',fit=crop'; + } + + $image_data['thumbnail'] = str_replace( $images[1][0], $image_url, $image_data['thumbnail'] ); + + return $image_data; + } +} diff --git a/app/integrations/class-rank-math.php b/app/integrations/class-rank-math.php index 1d6d5fc..38c03c2 100644 --- a/app/integrations/class-rank-math.php +++ b/app/integrations/class-rank-math.php @@ -17,6 +17,7 @@ use CF_Images\App\Traits; use Exception; use MyThemeShop\Helpers\Str; +use RankMath\Helper; use WP_Query; use function pathinfo; @@ -32,14 +33,25 @@ class Rank_Math { use Traits\Helpers; + /** + * Rank Math Image SEO active flag. + * + * @since 1.9.2 + * + * @var bool + */ + private $image_seo_active = false; + /** * Class constructor. * * @since 1.1.5 */ public function __construct() { + add_action( 'init', array( $this, 'is_image_seo_active' ) ); add_filter( 'rank_math/replacements', array( $this, 'fix_file_name_replacement' ), 10, 2 ); add_filter( 'cf_images_can_run', array( $this, 'can_run' ) ); + add_action( 'cf_images_get_attachment_image_src', array( $this, 'cache_image_ids' ), 10, 2 ); } /** @@ -100,12 +112,21 @@ private function get_image_id_from_url( string $image ) { return false; } - preg_match( '/\/([^\/]+?)\/[^\/]+$/', $image, $matches ); + $url_path = wp_parse_url( $image, PHP_URL_PATH ); + $pattern = '/\/[a-zA-Z0-9-]+\/([^\/]+(?:\/[^\/]+)*\.[a-z]+|[^\/]+)(?:\/.*)?/'; + + preg_match( $pattern, $url_path, $matches ); if ( ! isset( $matches[1] ) ) { return false; } + $post_id = wp_cache_get( $matches[1], 'cf_images' ); + + if ( false !== $post_id ) { + return $post_id; + } + $args = array( 'fields' => 'ids', 'meta_key' => '_cloudflare_image_id', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key @@ -124,6 +145,8 @@ private function get_image_id_from_url( string $image ) { return false; } + wp_cache_add( $matches[1], $results->posts[0], 'cf_images' ); + return $results->posts[0]; } @@ -154,4 +177,36 @@ private function get_filename( string $file ) { return '' !== $name ? $name : null; } + + /** + * Check is Rank Math Image SEO is active. + * + * @since 1.9.2 + */ + public function is_image_seo_active() { + if ( ! method_exists( '\RankMath\Helper', 'get_settings' ) ) { + return; + } + + $is_alt = Helper::get_settings( 'general.add_img_alt' ) && Helper::get_settings( 'general.img_alt_format' ) && trim( Helper::get_settings( 'general.img_alt_format' ) ); + $is_title = Helper::get_settings( 'general.add_img_title' ) && Helper::get_settings( 'general.img_title_format' ) && trim( Helper::get_settings( 'general.img_title_format' ) ); + + $this->image_seo_active = $is_alt || $is_title; + } + + /** + * Cache Cloudflare Image ID and WordPress attachment ID. + * + * @since 1.9.2 + * + * @param string $cloudflare_image_id Cloudflare Image ID. + * @param int|string $attachment_id Attachment ID. + */ + public function cache_image_ids( string $cloudflare_image_id, $attachment_id ) { + if ( ! $this->image_seo_active ) { + return; + } + + wp_cache_add( $cloudflare_image_id, $attachment_id, 'cf_images' ); + } } diff --git a/app/modules/class-cloudflare-images.php b/app/modules/class-cloudflare-images.php index 9d7f42b..75500c6 100644 --- a/app/modules/class-cloudflare-images.php +++ b/app/modules/class-cloudflare-images.php @@ -159,6 +159,8 @@ public function get_attachment_image_src( $image, $attachment_id, $size ) { return $image; } + do_action( 'cf_images_get_attachment_image_src', $cloudflare_image_id, $attachment_id ); + $cf_image = trailingslashit( $this->get_cdn_domain() . "/$hash" ) . $cloudflare_image_id; // If this is a known crop image. diff --git a/app/modules/class-image-ai.php b/app/modules/class-image-ai.php index 56d7167..2bfaad2 100644 --- a/app/modules/class-image-ai.php +++ b/app/modules/class-image-ai.php @@ -147,10 +147,10 @@ private function caption_image( int $attachment_id ) { list( $hash, $cloudflare_image_id ) = Cloudflare_Images::get_hash_id_url_string( $attachment_id ); if ( empty( $cloudflare_image_id ) || ( empty( $hash ) && ! $this->is_module_enabled( false, 'custom-path' ) ) ) { - $image = wp_get_original_image_url( $attachment_id ); + $image = wp_get_attachment_image_url( $attachment_id, 'full' ); } else { // Use the default Cloudflare Images URL here, so we do not get issues with access. - $image = trailingslashit( "https://imagedelivery.net/$hash" ) . "$cloudflare_image_id/w=9999"; + $image = trailingslashit( "https://imagedelivery.net/$hash" ) . "$cloudflare_image_id/w=2560"; } if ( $restore_filter ) { @@ -230,6 +230,9 @@ public function add_wp_query_args( array $args, string $action ): array { return $args; } + // Allow only supported mime types. + $args['post_mime_type'] = array( 'image/jpeg', 'image/png', 'image/gif', 'image/bmp' ); + $args['meta_query'] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query array( 'key' => '_wp_attachment_image_alt', diff --git a/cf-images.php b/cf-images.php index 27df360..d796dc8 100644 --- a/cf-images.php +++ b/cf-images.php @@ -14,7 +14,7 @@ * Plugin Name: Offload Media to Cloudflare Images * Plugin URI: https://vcore.au * Description: Offload media library images to the `Cloudflare Images` service. - * Version: 1.9.1 + * Version: 1.9.2 * Author: Anton Vanyukov * Author URI: https://vcore.au * License: GPL-2.0+ @@ -31,7 +31,7 @@ die; } -define( 'CF_IMAGES_VERSION', '1.9.1' ); +define( 'CF_IMAGES_VERSION', '1.9.2' ); define( 'CF_IMAGES_DIR_URL', plugin_dir_url( __FILE__ ) ); require_once 'app/class-activator.php'; diff --git a/package.json b/package.json index 7abcf72..e5ee6d6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cf-images", "description": "Offload, Store, Resize & Optimize with Cloudflare Images", - "version": "1.9.1", + "version": "1.9.2", "main": "cf-images.php", "author": "Anton Vanyukov", "license": "GPL-2.0-or-later", @@ -10,26 +10,26 @@ "optimization" ], "devDependencies": { - "@babel/plugin-transform-react-jsx": "^7.23.4", - "@babel/preset-env": "^7.24.3", - "@babel/preset-typescript": "^7.24.1", + "@babel/plugin-transform-react-jsx": "^7.24.7", + "@babel/preset-env": "^7.24.7", + "@babel/preset-typescript": "^7.24.7", "@creativebulma/bulma-tooltip": "^1.2.0", - "@types/jquery": "^3.5.29", - "@types/react": "^18.2.67", - "@types/react-dom": "^18.2.22", - "@wordpress/babel-plugin-import-jsx-pragma": "^4.37.0", - "@wordpress/eslint-plugin": "^17.11.0", - "@wordpress/i18n": "^4.54.0", - "@wordpress/prettier-config": "^3.11.0", + "@types/jquery": "^3.5.30", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@wordpress/babel-plugin-import-jsx-pragma": "^5.2.0", + "@wordpress/eslint-plugin": "^19.2.0", + "@wordpress/i18n": "^5.2.0", + "@wordpress/prettier-config": "^4.2.0", "babel-loader": "^9.1.3", - "css-loader": "^6.10.0", - "mini-css-extract-plugin": "^2.8.1", - "prettier": "^3.2.5", - "sass": "^1.72.0", - "sass-loader": "^14.1.1", + "css-loader": "^7.1.2", + "mini-css-extract-plugin": "^2.9.0", + "prettier": "^3.3.2", + "sass": "^1.77.6", + "sass-loader": "^14.2.1", "terser-webpack-plugin": "^5.3.10", - "typescript": "^5.4.3", - "webpack": "^5.91.0", + "typescript": "^5.5.3", + "webpack": "^5.92.1", "webpack-cli": "^5.1.4" }, "scripts": { @@ -44,9 +44,9 @@ "bulma": "^0.9.4", "bulma-switch": "^2.0.4", "classnames": "^2.5.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router-dom": "^6.22.3" + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.24.0" }, "babel": { "presets": [