diff --git a/CHANGELOG.md b/CHANGELOG.md index aa870b5..66c54d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ += 1.9.0 - 22.03.2024 = + +Added: +* Set browser TTL for images +* Option to serve originals for logged-in users +* Option to apply settings network wide in multisite + +Changed: +* Disable logging in wp-admin +* Improve detection of cropped images +* Fallback to scaled images if original image is larger than 20 Mb + +Fixed: +* Image size can now be changed in the Gutenberg image block for fully offloaded images +* Full size images not replaced in the gallery block on expand +* Multiple fixes and improvements with the WPML integration + = 1.8.0 - 16.02.2024 = Added: diff --git a/README.md b/README.md index 5372f58..ed46325 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Donate link: https://www.paypal.com/donate/?business=JRR6QPRGTZ46N&no_recurring= Requires at least: 5.6 Requires PHP: 7.0 Tested up to: 6.5 -Stable tag: 1.8.0 +Stable tag: 1.9.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -51,9 +51,7 @@ Cloudflare, the Cloudflare logo, and Cloudflare Workers are trademarks and/or re Special thanks to the plugin sponsors: - - - +WordPress Agency this:matters == Installation == @@ -104,6 +102,23 @@ If something is still not working for you, please let me know by creating a supp == Changelog == += 1.9.0 - 22.03.2024 = + +Added: +* Set browser TTL for images +* Option to serve originals for logged-in users +* Option to apply settings network wide in multisite + +Changed: +* Disable logging in wp-admin +* Improve detection of cropped images +* Fallback to scaled images if original image is larger than 20 Mb + +Fixed: +* Image size can now be changed in the Gutenberg image block for fully offloaded images +* Full size images not replaced in the gallery block on expand +* Multiple fixes and improvements with the WPML integration + = 1.8.0 - 16.02.2024 = Added: @@ -161,30 +176,6 @@ Fixed: * Bulk processing stops if an image triggers an error during upload * Settings resetting on update after using a beta version -= 1.5.1 - 28.10.2023 = - -Fixed: -* Do not replace images on wp-admin if full offload module is disabled - -= 1.5.0 - 27.10.2023 = - -Added: -* New and improved React-based UI -* Image compression module: optimize the size of your media library images -* WP CLI support via the "wp cf-images" commands (bulk & individual offload) -* Compatibility with the "Enable Media Replace" plugin -* Option to bulk add image captions -* Allow viewing a page with original images, using a "?cf-images-disable=true" URL query - -Changed: -* The "Auto resize images on front-end" module has been refactored to prevent double loading of images - -Fixed: -* Cropped image detection -* Compatibility with latest WordPress coding standards -* PHP warnings with page parser module on pages with no images -* Link for adding API key for AI module was not working - [Full changelog](https://github.com/av3nger/cf-images/blob/master/CHANGELOG.md). == Upgrade Notice == diff --git a/app/api/class-variant.php b/app/api/class-variant.php index 2d3138a..b72745d 100644 --- a/app/api/class-variant.php +++ b/app/api/class-variant.php @@ -54,4 +54,27 @@ public function toggle_flexible( bool $value ): stdClass { return $this->request(); } + + /** + * Set images cache TTL. + * + * @since 1.9.0 + * + * @param int $value Cache TTL in seconds. + * + * @return stdClass + * @throws Exception Exception during API call. + */ + public function set_cache_ttl( int $value ): stdClass { + $data = array( + 'browser_ttl' => $value, + ); + + $this->set_method( 'PATCH' ); + $this->set_timeout( 2 ); + $this->set_endpoint( '/config' ); + $this->set_request_body( wp_json_encode( $data ) ); + + return $this->request(); + } } diff --git a/app/class-admin.php b/app/class-admin.php index 7312676..ef8829a 100644 --- a/app/class-admin.php +++ b/app/class-admin.php @@ -127,15 +127,17 @@ public function enqueue_scripts( string $hook ) { $this->get_slug(), 'CFImages', array( - 'nonce' => wp_create_nonce( 'cf-images-nonce' ), - 'dirURL' => CF_IMAGES_DIR_URL, - 'settings' => get_option( 'cf-images-settings', Settings::get_defaults() ), - 'cfStatus' => $this->is_set_up(), - 'domain' => get_option( 'cf-images-custom-domain', '' ), - 'hideSidebar' => get_site_option( 'cf-images-hide-sidebar' ), - 'fuzion' => $this->is_fuzion_api_connected(), - 'stats' => $this->get_stats(), - 'cdnEnabled' => (bool) get_option( 'cf-images-cdn-enabled', false ), + 'nonce' => wp_create_nonce( 'cf-images-nonce' ), + 'dirURL' => CF_IMAGES_DIR_URL, + 'settings' => apply_filters( 'cf_images_settings', get_option( 'cf-images-settings', Settings::get_defaults() ) ), + 'cfStatus' => $this->is_set_up(), + 'domain' => get_option( 'cf-images-custom-domain', '' ), + 'hideSidebar' => get_site_option( 'cf-images-hide-sidebar' ), + 'fuzion' => $this->is_fuzion_api_connected(), + 'stats' => $this->get_stats(), + 'cdnEnabled' => (bool) get_option( 'cf-images-cdn-enabled', false ), + 'isNetworkAdmin' => is_multisite() && is_main_site(), + 'browserTTL' => get_site_option( 'cf-images-browser-ttl', 172800 ), ) ); } @@ -150,6 +152,10 @@ public function enqueue_scripts( string $hook ) { * @return array */ public function settings_link( array $actions ): array { + if ( $this->is_network_wide() ) { + return $actions; + } + if ( ! current_user_can( 'manage_options' ) ) { return $actions; } @@ -164,6 +170,10 @@ public function settings_link( array $actions ): array { * @since 1.0.0 */ public function register_menu() { + if ( $this->is_network_wide() ) { + return; + } + add_submenu_page( 'upload.php', __( 'Offload Images to Cloudflare', 'cf-images' ), diff --git a/app/class-core.php b/app/class-core.php index 018cd30..4c7b3ae 100644 --- a/app/class-core.php +++ b/app/class-core.php @@ -210,10 +210,13 @@ private function init_integrations() { * @see Modules\Service * @see Modules\CDN * @see Modules\Full_Offload + * @see Modules\Multisite + * @see Modules\Cache_TTL */ private function load_modules() { $loader = Loader::get_instance(); + $loader->module( 'multisite' ); // This should be loaded before other modules. $loader->module( 'cdn' ); // This should be loaded before other modules. $loader->module( 'auto-offload' ); $loader->module( 'auto-resize' ); @@ -229,6 +232,7 @@ private function load_modules() { $loader->module( 'custom-path' ); $loader->module( 'service' ); $loader->module( 'full-offload' ); + $loader->module( 'cache-ttl' ); } /** diff --git a/app/class-media.php b/app/class-media.php index fe2f7a9..86385e7 100644 --- a/app/class-media.php +++ b/app/class-media.php @@ -395,6 +395,9 @@ public function upload_image( $metadata, int $attachment_id, string $action = '' return $metadata; } + // This is used with WPML integration. + $attachment_id = apply_filters( 'cf_images_media_post_id', $attachment_id ); + $mime = get_post_mime_type( $attachment_id ); if ( ! wp_attachment_is_image( $attachment_id ) || false !== strpos( $mime, 'image/svg' ) ) { @@ -407,6 +410,10 @@ public function upload_image( $metadata, int $attachment_id, string $action = '' $dir = wp_get_upload_dir(); $path = wp_get_original_image_path( $attachment_id ); + if ( file_exists( $path ) && ( MB_IN_BYTES * 20 ) <= filesize( $path ) ) { + $path = get_attached_file( $attachment_id ); + } + $url = wp_parse_url( get_site_url() ); if ( is_multisite() && ! is_subdomain_install() ) { $host = $url['host'] . $url['path']; @@ -429,6 +436,8 @@ public function upload_image( $metadata, int $attachment_id, string $action = '' update_post_meta( $attachment_id, '_cloudflare_image_id', $results->id ); $this->maybe_save_hash( $results->variants ); + do_action( 'cf_images_upload_success', $attachment_id, $results ); + if ( doing_filter( 'wp_async_wp_generate_attachment_metadata' ) ) { $this->fetch_stats( new Api\Image() ); } @@ -461,6 +470,8 @@ public function remove_from_cloudflare( int $post_id ) { delete_post_meta( $post_id, '_cloudflare_image_id' ); delete_post_meta( $post_id, '_cloudflare_image_skip' ); + do_action( 'cf_images_remove_success', $post_id ); + if ( doing_action( 'delete_attachment' ) ) { $this->fetch_stats( new Api\Image() ); } diff --git a/app/class-settings.php b/app/class-settings.php index 4a68882..a83add8 100644 --- a/app/class-settings.php +++ b/app/class-settings.php @@ -48,6 +48,7 @@ class Settings { 'image-generate' => false, 'logging' => false, 'rss-feeds' => false, + 'no-offload-user' => false, // Do not offload images for logged-in users. ); /** diff --git a/app/integrations/class-wpml.php b/app/integrations/class-wpml.php index 335c525..afebd0a 100644 --- a/app/integrations/class-wpml.php +++ b/app/integrations/class-wpml.php @@ -14,6 +14,8 @@ namespace CF_Images\App\Integrations; +use stdClass; + if ( ! defined( 'WPINC' ) ) { die; } @@ -32,8 +34,9 @@ class Wpml { public function __construct() { add_filter( 'cf_images_media_post_id', array( $this, 'get_original_image_id' ) ); add_action( 'cf_images_before_wp_query', array( $this, 'remove_wpml_filters' ) ); - add_action( 'wpml_after_duplicate_attachment', array( $this, 'ignore_attachment' ), 10, 2 ); - add_action( 'wpml_after_copy_attached_file_postmeta', array( $this, 'ignore_attachment' ), 10, 2 ); + add_action( 'cf_images_upload_success', array( $this, 'update_image_meta' ), 10, 2 ); + add_action( 'cf_images_remove_success', array( $this, 'image_removed_from_cf' ) ); + add_filter( 'cf_images_wp_query_args', array( $this, 'add_wp_query_args' ), 10, 2 ); } /** @@ -75,16 +78,85 @@ public function remove_wpml_filters() { } /** - * Fires when an attachment is duplicated. + * Get translations for an image. * - * Duplicated images do not need to be processed, otherwise this causes double uploads to Cloudflare. + * @since 1.9.0 * - * @param int $attachment_id The ID of the source/original attachment. - * @param int $duplicated_attachment_id The ID of the duplicated attachment. + * @param int $attachment_id Attachment ID. * - * @since 1.4.0 + * @return array + */ + private function get_translations( int $attachment_id ): array { + global $sitepress; + + if ( ! $sitepress || ! method_exists( $sitepress, 'get_element_trid' ) ) { + return array(); + } + + $translation_id = $sitepress->get_element_trid( $attachment_id, 'post_attachment' ); + return $sitepress->get_element_translations( $translation_id, 'post_attachment', true ); + } + + /** + * Update the meta for all images. + * + * @since 1.9.0 + * + * @param int $attachment_id Original attachment ID. + * @param stdClass $results Upload results. + */ + public function update_image_meta( int $attachment_id, stdClass $results ) { + $translations = $this->get_translations( $attachment_id ); + + foreach ( $translations as $translation ) { + if ( $translation->original ) { + continue; + } + + update_post_meta( $translation->element_id, '_cloudflare_image_id', $results->id ); + } + } + + /** + * Remove meta from all translatable images when the main image is removed from Cloudflare. + * + * @since 1.9.0 + * + * @param int $attachment_id Attachment ID. + */ + public function image_removed_from_cf( int $attachment_id ) { + $translations = $this->get_translations( $attachment_id ); + + foreach ( $translations as $translation ) { + if ( $translation->original ) { + continue; + } + + delete_post_meta( $translation->element_id, '_cloudflare_image_id' ); + } + } + + /** + * Adjust the WP_Query args for bulk offload action. + * + * @since 1.9.0 + * @see Ajax::get_wp_query_args() + * + * @param array $args WP_Query args. + * @param string $action Executing action. + * + * @return array */ - public function ignore_attachment( int $attachment_id, int $duplicated_attachment_id ) { - update_post_meta( $duplicated_attachment_id, '_cloudflare_image_skip', true ); + public function add_wp_query_args( array $args, string $action ): array { + if ( 'upload' !== $action ) { + return $args; + } + + $args['meta_query'][] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'compare' => 'NOT EXISTS', + 'key' => 'wpml_media_processed', + ); + + return $args; } } diff --git a/app/modules/class-cache-ttl.php b/app/modules/class-cache-ttl.php new file mode 100644 index 0000000..238d32b --- /dev/null +++ b/app/modules/class-cache-ttl.php @@ -0,0 +1,65 @@ + + * @since 1.9.0 + */ + +namespace CF_Images\App\Modules; + +use CF_Images\App\Api; +use CF_Images\App\Traits; +use Exception; + +if ( ! defined( 'WPINC' ) ) { + die; +} + +/** + * Cache_TTL class. + * + * @since 1.9.0 + */ +class Cache_TTL extends Module { + use Traits\Ajax; + use Traits\Empty_Init; + use Traits\Helpers; + + /** + * Run everything regardless of module status. + * + * @since 1.9.0 + */ + public function pre_init() { + if ( wp_doing_ajax() ) { + add_action( 'wp_ajax_cf_images_set_ttl', array( $this, 'ajax_set_ttl' ) ); + } + } + + /** + * Set browser cache TTL. + * + * @since 1.9.0 + */ + public function ajax_set_ttl() { + $this->check_ajax_request(); + + $data = filter_input( INPUT_POST, 'data', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY ); + $ttl = $data['ttl'] ? (int) $data['ttl'] : 172800; + + try { + ( new Api\Variant() )->set_cache_ttl( $ttl ); + update_site_option( 'cf-images-browser-ttl', $ttl ); + } catch ( Exception $e ) { + wp_send_json_error( $e->getMessage() ); + } + } +} diff --git a/app/modules/class-cloudflare-images.php b/app/modules/class-cloudflare-images.php index 79a3fca..4ccfb8a 100644 --- a/app/modules/class-cloudflare-images.php +++ b/app/modules/class-cloudflare-images.php @@ -92,7 +92,7 @@ protected function pre_init() { public function init() { add_action( 'init', array( $this, 'populate_image_sizes' ) ); - if ( filter_input( INPUT_GET, 'cf-images-disable' ) ) { + if ( ! $this->can_offload() ) { return; } @@ -101,6 +101,9 @@ public function init() { add_filter( 'wp_prepare_attachment_for_js', array( $this, 'prepare_attachment_for_js' ), 10, 2 ); add_filter( 'wp_calculate_image_srcset', array( $this, 'calculate_image_srcset' ), 10, 5 ); + // Support for various Gutenberg blocks. + add_filter( 'wp_get_attachment_url', array( $this, 'get_attachment_url' ), 10, 2 ); + // This filter is available on WordPress 6.0 or above. add_filter( 'wp_content_img_tag', array( $this, 'content_img_tag' ), 10, 3 ); @@ -141,7 +144,7 @@ public function populate_image_sizes() { * @return array|false */ public function get_attachment_image_src( $image, $attachment_id, $size ) { - if ( ! $this->can_run() || ! $image ) { + if ( ! $this->can_run( (int) $attachment_id ) || ! $image ) { do_action( 'cf_images_log', 'Cannot run get_attachment_image_src(), returning original. Attachment ID: %s. Image: %s', $attachment_id, $image ); return $image; } @@ -163,7 +166,12 @@ public function get_attachment_image_src( $image, $attachment_id, $size ) { // Image with defined dimensions. if ( isset( $image[1] ) && $image[1] > 0 ) { - $image[0] = $cf_image . '/w=' . $image[1]; + $height_str = ''; + if ( isset( $image[2] ) && $image[2] > 0 ) { + $height_str = ',h=' . $image[2] . ( $image[1] === $image[2] ? ',fit=crop' : '' ); + } + + $image[0] = $cf_image . '/w=' . $image[1] . $height_str; return $image; } @@ -366,4 +374,24 @@ public function preconnect( array $hints, string $relation_type ): array { ) ); } + + /** + * Filters the attachment URL. + * + * @since 1.9.0 + * + * @param string $url URL for the given attachment. + * @param int $attachment_id Attachment post ID. + * + * @return string + */ + public function get_attachment_url( string $url, int $attachment_id ): string { + if ( is_admin() ) { + return $url; + } + + $image_src = $this->get_attachment_image_src( array( $url ), $attachment_id, null ); + + return $image_src[0]; + } } diff --git a/app/modules/class-disable-async.php b/app/modules/class-disable-async.php index 23b2e0b..cf6cc80 100644 --- a/app/modules/class-disable-async.php +++ b/app/modules/class-disable-async.php @@ -14,6 +14,7 @@ namespace CF_Images\App\Modules; use CF_Images\App\Async; +use CF_Images\App\Traits\Empty_Init; if ( ! defined( 'WPINC' ) ) { die; @@ -25,12 +26,7 @@ * @since 1.4.0 */ class Disable_Async extends Module { - /** - * Init the module. - * - * @since 1.4.0 - */ - public function init() {} + use Empty_Init; /** * Because the actions need to run if this module is disabled (which is reverse of a typical module), diff --git a/app/modules/class-logging.php b/app/modules/class-logging.php index 935b00a..d68e3aa 100644 --- a/app/modules/class-logging.php +++ b/app/modules/class-logging.php @@ -82,7 +82,7 @@ private function init_log_file() { * @return void */ public function log( $message, ...$args ) { - if ( empty( $message ) && empty( $args ) ) { + if ( ( empty( $message ) && empty( $args ) ) || is_admin() ) { return; } diff --git a/app/modules/class-module.php b/app/modules/class-module.php index 152fdc9..b0ebf6c 100644 --- a/app/modules/class-module.php +++ b/app/modules/class-module.php @@ -14,6 +14,7 @@ namespace CF_Images\App\Modules; +use CF_Images\App\Settings; use CF_Images\App\Traits\Helpers; if ( ! defined( 'WPINC' ) ) { @@ -102,10 +103,12 @@ protected function pre_init() {} * * @since 1.1.3 * + * @param int $attachment_id Optional. Attachment ID. + * * @return bool */ - protected function can_run(): bool { - if ( $this->is_rest_request() || wp_doing_cron() ) { + protected function can_run( int $attachment_id = 0 ): bool { + if ( $this->is_rest_request( $attachment_id ) || wp_doing_cron() ) { return false; } @@ -122,9 +125,19 @@ protected function can_run(): bool { * * @since 1.2.0 * + * @param int $attachment_id Optional. Attachment ID. + * * @return bool */ - private function is_rest_request(): bool { + private function is_rest_request( int $attachment_id = 0 ): bool { + if ( $attachment_id ) { + // We must rely on the REST API endpoints if full offload is enabled. + $deleted = get_post_meta( $attachment_id, '_cloudflare_image_offloaded', true ); + if ( $deleted && apply_filters( 'cf_images_module_enabled', false, 'full-offload' ) ) { + return false; + } + } + $wordpress_has_no_logic = filter_input( INPUT_GET, '_wp-find-template' ); $wordpress_has_no_logic = sanitize_key( $wordpress_has_no_logic ); @@ -163,8 +176,33 @@ public function is_module_enabled( bool $fallback = false, string $module = '' ) $module = $this->module; } - $settings = get_option( 'cf-images-settings', \CF_Images\App\Settings::get_defaults() ); + $settings = apply_filters( 'cf_images_settings', get_option( 'cf-images-settings', Settings::get_defaults() ) ); return apply_filters( 'cf_images_module_status', $settings[ $module ] ?? $fallback, $module ); } + + /** + * In certain cases offloading should be disabled. + * + * @since 1.9.0 + * + * @return bool + */ + public function can_offload(): bool { + if ( filter_input( INPUT_GET, 'cf-images-disable' ) ) { + return false; + } + + // Full offload overrides all other settings, because there are no local images available. + if ( $this->is_module_enabled( false, 'full-offload' ) ) { + return true; + } + + if ( $this->is_module_enabled( false, 'no-offload-user' ) && defined( 'LOGGED_IN_COOKIE' ) ) { + // Check logged-in user cookie. + return empty( $_COOKIE[ LOGGED_IN_COOKIE ] ); + } + + return true; + } } diff --git a/app/modules/class-multisite.php b/app/modules/class-multisite.php new file mode 100644 index 0000000..691189f --- /dev/null +++ b/app/modules/class-multisite.php @@ -0,0 +1,99 @@ + + * + * @since 1.9.0 + */ + +namespace CF_Images\App\Modules; + +use CF_Images\App\Settings; +use CF_Images\App\Traits\Empty_Init; + +if ( ! defined( 'WPINC' ) ) { + die; +} + +/** + * Multisite class. + * + * @since 1.9.0 + */ +class Multisite extends Module { + use Empty_Init; + + /** + * This is a core module, meaning it can't be enabled/disabled via options. + * + * @since 1.9.0 + * + * @var bool + */ + protected $core = true; + + /** + * Run everything regardless of module status. + * + * @since 1.9.0 + */ + public function pre_init() { + add_action( 'cf_images_save_settings', array( $this, 'on_settings_update' ), 10, 2 ); + add_filter( 'cf_images_settings', array( $this, 'network_settings' ) ); + } + + /** + * Update the module status. + * + * @since 1.9.0 + * + * @param array $settings Settings array. + * @param array $data Passed in data from the app. + */ + public function on_settings_update( array $settings, array $data ) { + if ( ! is_multisite() || ! is_main_site() ) { + return; + } + + if ( ! isset( $data['network-wide'] ) || ! filter_var( $data['network-wide'], FILTER_VALIDATE_BOOLEAN ) ) { + delete_site_option( 'cf-images-network-wide' ); + } else { + update_site_option( 'cf-images-network-wide', true ); + } + } + + /** + * Use network wide settings. + * + * @since 1.9.0 + * + * @param array $settings Current settings. + * + * @return array + */ + public function network_settings( array $settings ): array { + if ( ! is_multisite() ) { + return $settings; + } + + if ( is_main_site() ) { + $settings['network-wide'] = get_site_option( 'cf-images-network-wide' ); + return $settings; + } + + $main_site_id = get_main_site_id(); + + switch_to_blog( $main_site_id ); + + $settings = get_option( 'cf-images-settings', Settings::get_defaults() ); + + restore_current_blog(); + + return $settings; + } +} diff --git a/app/modules/class-page-parser.php b/app/modules/class-page-parser.php index b434be2..d560b51 100644 --- a/app/modules/class-page-parser.php +++ b/app/modules/class-page-parser.php @@ -51,7 +51,7 @@ public function pre_init() { * @since 1.4.0 */ public function init() { - if ( filter_input( INPUT_GET, 'cf-images-disable' ) ) { + if ( ! $this->can_offload() ) { return; } diff --git a/app/modules/class-service.php b/app/modules/class-service.php index 7d6ed60..4742876 100644 --- a/app/modules/class-service.php +++ b/app/modules/class-service.php @@ -28,13 +28,7 @@ */ class Service extends Module { use Traits\Ajax; - - /** - * Init the module. - * - * @since 1.7.0 - */ - public function init() {} + use Traits\Empty_Init; /** * Init the module. diff --git a/app/traits/trait-empty-init.php b/app/traits/trait-empty-init.php new file mode 100644 index 0000000..e8927a7 --- /dev/null +++ b/app/traits/trait-empty-init.php @@ -0,0 +1,31 @@ + + * @since 1.9.0 + */ + +namespace CF_Images\App\Traits; + +if ( ! defined( 'WPINC' ) ) { + die; +} + +/** + * Empty_Init trait. + * + * @since 1.9.0 + */ +trait Empty_Init { + /** + * Init module. + * + * @since 1.9.0 + */ + public function init() {} +} diff --git a/app/traits/trait-helpers.php b/app/traits/trait-helpers.php index 14e5e3d..f7eacf2 100644 --- a/app/traits/trait-helpers.php +++ b/app/traits/trait-helpers.php @@ -14,6 +14,7 @@ use CF_Images\App\Core; use CF_Images\App\Media; +use CF_Images\App\Settings; use WP_Error; if ( ! defined( 'WPINC' ) ) { @@ -110,4 +111,19 @@ protected function is_fuzion_api_connected(): bool { protected function media(): Media { return Core::get_instance()->admin()->media(); } + + /** + * Check if network wide settings enabled. + * + * @since 1.9.0 + * + * @return bool + */ + protected function is_network_wide(): bool { + if ( ! is_multisite() || is_main_site() ) { + return false; + } + + return get_site_option( 'cf-images-network-wide' ); + } } diff --git a/assets/_src/app.scss b/assets/_src/app.scss index e419a66..483d9b1 100644 --- a/assets/_src/app.scss +++ b/assets/_src/app.scss @@ -24,6 +24,7 @@ $switch-background-active: $blue; @import "bulma/sass/elements/progress"; @import "bulma/sass/form/shared"; @import "bulma/sass/form/input-textarea"; + @import "bulma/sass/form/select"; @import "bulma/sass/form/tools"; @import "bulma/sass/components/card"; @import "bulma/sass/components/media"; @@ -55,6 +56,10 @@ $switch-background-active: $blue; } } + select { + background: none !important; + } + // Fix bulma-switch issues. & .switch[type=checkbox]+label { padding-top: 0; diff --git a/assets/_src/context/provider.tsx b/assets/_src/context/provider.tsx index e3f300b..a6f6b24 100644 --- a/assets/_src/context/provider.tsx +++ b/assets/_src/context/provider.tsx @@ -17,7 +17,14 @@ import SettingsContext from './settings'; * @class */ const SettingsProvider = ({ children }: { children: ReactElement[] }) => { - const { cfStatus, fuzion, hideSidebar, settings } = window.CFImages; + const { + browserTTL, + cfStatus, + fuzion, + hideSidebar, + isNetworkAdmin, + settings, + } = window.CFImages; const [stats, setStats] = useState(window.CFImages.stats); const [modules, setModules] = useState(settings); @@ -44,11 +51,13 @@ const SettingsProvider = ({ children }: { children: ReactElement[] }) => { return ( void; noticeHidden: boolean; @@ -31,6 +34,7 @@ interface SettingsContextType { cfConnected: boolean; setCfConnected: (status: boolean) => void; inProgress: boolean; + isNetworkAdmin: boolean; setInProgress: (status: boolean) => void; stats: StatsType; setStats: (stats: StatsType) => void; diff --git a/assets/_src/modules/cloudflare/misc.tsx b/assets/_src/modules/cloudflare/misc.tsx new file mode 100644 index 0000000..932feca --- /dev/null +++ b/assets/_src/modules/cloudflare/misc.tsx @@ -0,0 +1,78 @@ +/** + * External dependencies + */ +import { useContext } from 'react'; +import Icon from '@mdi/react'; +import { mdiInformationOutline, mdiCogs } from '@mdi/js'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import Card from '../../components/card'; +import SettingsContext from '../../context/settings'; + +const MiscOptions = () => { + const { isNetworkAdmin, modules, setModule } = useContext(SettingsContext); + + const subModules = { + 'rss-feeds': { + label: __('RSS Feeds', 'cf-images'), + description: __('Replace images inside RSS feeds.', 'cf-images'), + }, + 'no-offload-user': { + label: __('Skip logged-in', 'cf-images'), + description: __( + 'Serve original (non-offloaded) images for logged-in users.', + 'cf-images' + ), + }, + }; + + if (isNetworkAdmin) { + subModules['network-wide'] = { + label: __('Network wide settings', 'cf-images'), + description: __( + 'Apply the above settings to all sub-sites in the network and hide the offload media menu option.', + 'cf-images' + ), + }; + } + + const options = Object.entries(subModules).map((module) => { + const { label, description } = module[1]; + return ( + + setModule(module[0], e.target.checked)} + type="checkbox" + /> + + {label} + + + + + + ); + }); + + return ( + + {options} + + ); +}; + +export default MiscOptions; diff --git a/assets/_src/modules/cloudflare/rss-feeds.tsx b/assets/_src/modules/cloudflare/rss-feeds.tsx deleted file mode 100644 index 5f05bd2..0000000 --- a/assets/_src/modules/cloudflare/rss-feeds.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/** - * External dependencies - */ -import { mdiRss } from '@mdi/js'; - -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import Card from '../../components/card'; - -const RSSFeeds = () => { - return ( - - - {__('Replace images inside RSS feeds.', 'cf-images')} - - - ); -}; - -export default RSSFeeds; diff --git a/assets/_src/modules/cloudflare/ttl.tsx b/assets/_src/modules/cloudflare/ttl.tsx new file mode 100644 index 0000000..ea91632 --- /dev/null +++ b/assets/_src/modules/cloudflare/ttl.tsx @@ -0,0 +1,121 @@ +/** + * External dependencies + */ +import { useContext, useState } from 'react'; +import * as classNames from 'classnames'; +import { mdiCached } from '@mdi/js'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { post } from '../../js/helpers/post'; +import Card from '../../components/card'; +import SettingsContext from '../../context/settings'; + +const BrowserTTL = () => { + const { browserTTL } = useContext(SettingsContext); + + const [ttl, setTTL] = useState(parseInt(browserTTL)); + const [done, setDone] = useState(false); + const [error, setError] = useState(''); + const [saving, setSaving] = useState(false); + + const saveTTL = () => { + setError(''); + setSaving(true); + + post('cf_images_set_ttl', { ttl }) + .then((response: ApiResponse) => { + setSaving(false); + + if (!response.success && response.data) { + setError(response.data); + setTimeout(() => setError(''), 10000); + } else { + setDone(true); + setTimeout(() => setDone(false), 2000); + } + }) + .catch(window.console.log); + }; + + return ( + + + + {__( + 'Browser TTL controls how long an image stays in a browser’s cache and specifically configures the cache-control response header', + 'cf-images' + )} + + + + + + + {__('Set browser TTL', 'cf-images')} + + + setTTL(parseInt(e.target.value)) + } + > + + {__('2 days', 'cf-images')} + + + {__('1 week', 'cf-images')} + + + {__('1 month', 'cf-images')} + + + {__('1 year', 'cf-images')} + + + {error && {error}} + + + + + {__('Set', 'cf-images')} + + + + + + ); +}; + +export default BrowserTTL; diff --git a/assets/_src/routes/cloudflare/settings.tsx b/assets/_src/routes/cloudflare/settings.tsx index fa2d6ab..2e9f48a 100644 --- a/assets/_src/routes/cloudflare/settings.tsx +++ b/assets/_src/routes/cloudflare/settings.tsx @@ -22,7 +22,8 @@ import CloudflareDisconnect from '../../modules/actions/cf-disconnect'; import CloudflareStats from '../../modules/cloudflare/cf-stats'; import Logging from '../../modules/cloudflare/logging'; import Service from '../../modules/cloudflare/service'; -import RSSFeeds from '../../modules/cloudflare/rss-feeds'; +import MiscOptions from '../../modules/cloudflare/misc'; +import BrowserTTL from '../../modules/cloudflare/ttl'; /** * Cloudflare Images settings routes. @@ -48,12 +49,13 @@ const CloudflareSettings = () => { )} + - + diff --git a/cf-images.php b/cf-images.php index ececca4..7dd2b67 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.8.0 + * Version: 1.9.0 * Author: Anton Vanyukov * Author URI: https://vcore.au * License: GPL-2.0+ @@ -31,7 +31,7 @@ die; } -define( 'CF_IMAGES_VERSION', '1.8.0' ); +define( 'CF_IMAGES_VERSION', '1.9.0' ); define( 'CF_IMAGES_DIR_URL', plugin_dir_url( __FILE__ ) ); require_once 'app/class-activator.php'; @@ -49,6 +49,7 @@ */ function run_cf_images() { require_once __DIR__ . '/app/traits/trait-ajax.php'; + require_once __DIR__ . '/app/traits/trait-empty-init.php'; require_once __DIR__ . '/app/traits/trait-helpers.php'; require_once __DIR__ . '/app/traits/trait-stats.php'; require_once __DIR__ . '/app/class-core.php'; diff --git a/package.json b/package.json index f02450c..68fb71d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cf-images", "description": "Offload, Store, Resize & Optimize with Cloudflare Images", - "version": "1.8.0", + "version": "1.9.0", "main": "cf-images.php", "author": "Anton Vanyukov", "license": "GPL-2.0-or-later", @@ -11,25 +11,25 @@ ], "devDependencies": { "@babel/plugin-transform-react-jsx": "^7.23.4", - "@babel/preset-env": "^7.23.9", - "@babel/preset-typescript": "^7.23.3", + "@babel/preset-env": "^7.24.3", + "@babel/preset-typescript": "^7.24.1", "@creativebulma/bulma-tooltip": "^1.2.0", "@types/jquery": "^3.5.29", - "@types/react": "^18.2.55", - "@types/react-dom": "^18.2.19", - "@wordpress/babel-plugin-import-jsx-pragma": "^4.34.0", - "@wordpress/eslint-plugin": "^17.8.0", - "@wordpress/i18n": "^4.51.0", - "@wordpress/prettier-config": "^3.8.0", + "@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", "babel-loader": "^9.1.3", "css-loader": "^6.10.0", - "mini-css-extract-plugin": "^2.8.0", + "mini-css-extract-plugin": "^2.8.1", "prettier": "^3.2.5", - "sass": "^1.70.0", - "sass-loader": "^14.1.0", + "sass": "^1.72.0", + "sass-loader": "^14.1.1", "terser-webpack-plugin": "^5.3.10", - "typescript": "^5.3.3", - "webpack": "^5.90.1", + "typescript": "^5.4.3", + "webpack": "^5.91.0", "webpack-cli": "^5.1.4" }, "scripts": { @@ -46,7 +46,7 @@ "classnames": "^2.5.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.22.0" + "react-router-dom": "^6.22.3" }, "babel": { "presets": [ diff --git a/uninstall.php b/uninstall.php index 21c8144..3539cdd 100644 --- a/uninstall.php +++ b/uninstall.php @@ -33,6 +33,8 @@ delete_site_option( 'cf-images-hide-sidebar' ); delete_site_option( 'cf-images-account-id' ); delete_site_option( 'cf-images-api-token' ); +delete_site_option( 'cf-images-network-wide' ); +delete_site_option( 'cf-images-browser-ttl' ); delete_option( 'cf-images-custom-domain' ); delete_option( 'cf-images-setup-done' ); delete_option( 'cf-images-config-written' );
{__('Replace images inside RSS feeds.', 'cf-images')}
+ {__( + 'Browser TTL controls how long an image stays in a browser’s cache and specifically configures the cache-control response header', + 'cf-images' + )} +
{error}