* Created on: 19/07/2018 * * @soundtrack Somewhere Else - Marillion * @package \Optimole\Inc * @author Optimole */ use enshrined\svgSanitize\Sanitizer; use OptimoleWP\BgOptimizer\Lazyload; use OptimoleWP\PageProfiler\Profile; /** * Class Optml_Admin */ class Optml_Admin { use Optml_Normalizer; const IMAGE_DATA_COLLECTED = 'optml_image_data_collected'; const IMAGE_DATA_COLLECTED_BATCH = 100; const BF_PROMO_DISMISS_KEY = 'optml_bf25_notice_dismiss'; const SPC_BANNER_DISMISS_KEY = 'optml_spc_banner_dismiss'; /** * Hold the settings object. * * @var Optml_Settings Settings object. */ public $settings; /** * Hold the plugin conflict object. * * @var Optml_Conflicting_Plugins Settings object. */ public $conflicting_plugins; const NEW_USER_DEFAULTS_UPDATED = 'optml_defaults_updated'; const OLD_USER_ENABLED_LD = 'optml_enabled_limit_dimensions'; const OLD_USER_ENABLED_CL = 'optml_enabled_cloud_sites'; const SYNC_CRON = 'optml_daily_sync'; const ENRICH_CRON = 'optml_pull_image_data'; /** * Optml_Admin constructor. */ public function __construct() { $this->settings = new Optml_Settings(); $this->conflicting_plugins = new Optml_Conflicting_Plugins(); $media_rename = new Optml_Attachment_Edit(); $media_rename->init(); $dashboard_widget = new Optml_Dashboard_Widget(); $dashboard_widget->init(); add_filter( 'plugin_action_links_' . plugin_basename( OPTML_BASEFILE ), [ $this, 'add_action_links' ] ); add_action( 'admin_menu', [ $this, 'add_dashboard_page' ] ); add_action( 'admin_menu', [ $this, 'add_settings_subpage' ], 99 ); add_action( 'admin_head', [ $this, 'menu_icon_style' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue' ], PHP_INT_MIN ); add_action( 'admin_notices', [ $this, 'add_notice' ] ); add_action( 'admin_notices', [ $this, 'add_notice_upgrade' ] ); add_action( 'admin_notices', [ $this, 'add_notice_conflicts' ] ); add_action( self::SYNC_CRON, [ $this, 'daily_sync' ] ); add_action( 'admin_init', [ $this, 'redirect_old_dashboard' ] ); add_action( 'optml_purge_image_cache', [ $this, 'purge_image_cache' ] ); if ( $this->settings->is_connected() ) { add_action( 'init', [ $this, 'check_domain_change' ] ); add_action( self::ENRICH_CRON, [ $this, 'pull_image_data' ] ); // Backwards compatibility for older versions of WordPress < 6.0.0 requiring 3 parameters for this specific filter. $below_6_0_0 = version_compare( get_bloginfo( 'version' ), '6.0.0', '<' ); if ( $below_6_0_0 ) { add_filter( 'wp_insert_attachment_data', [ $this, 'legacy_detect_image_title_changes' ], 10, 3 ); } else { add_filter( 'wp_insert_attachment_data', [ $this, 'detect_image_title_changes' ], 10, 4 ); } add_action( 'updated_post_meta', [ $this, 'detect_image_alt_change' ], 10, 4 ); add_action( 'added_post_meta', [ $this, 'detect_image_alt_change' ], 10, 4 ); add_filter( 'update_attached_file', [ $this, 'listen_update_file' ], 999, 2 ); if ( ! wp_next_scheduled( self::ENRICH_CRON ) ) { wp_schedule_event( time() + 10, 'hourly', self::ENRICH_CRON ); } } add_action( 'init', [ $this, 'update_default_settings' ] ); add_action( 'init', [ $this, 'update_limit_dimensions' ] ); add_action( 'init', [ $this, 'update_cloud_sites_default' ] ); add_action( 'admin_init', [ $this, 'maybe_redirect' ] ); add_action( 'admin_init', [ $this, 'init_no_script' ] ); add_action( 'plugins_loaded', [ $this,'add_permission_policy' ] ); if ( ! is_admin() && $this->settings->is_connected() && ! wp_next_scheduled( self::SYNC_CRON ) ) { wp_schedule_event( time() + 10, 'twicedaily', self::SYNC_CRON, [] ); } add_action( 'optml_after_setup', [ $this, 'register_public_actions' ], 999999 ); if ( ! function_exists( 'is_wpcom_vip' ) ) { add_filter( 'upload_mimes', [ $this, 'allow_svg', ] ); // phpcs:ignore WordPressVIPMinimum.Hooks.RestrictedHooks.upload_mimes add_filter( 'wp_handle_upload_prefilter', [ $this, 'check_svg_and_sanitize' ] ); } add_filter( 'themeisle-sdk/survey/' . OPTML_PRODUCT_SLUG, [ $this, 'get_survey_metadata' ], 10, 2 ); add_action( 'admin_init', [ $this, 'mark_user_with_offload' ] ); } /** * Add the permission policy for CH. * * @return void */ public function add_permission_policy() { if ( ! $this->settings->is_connected() ) { return; } if ( ! $this->settings->is_enabled() ) { return; } if ( headers_sent() ) { return; } $policy = $this->get_permissions_policy(); if ( empty( $policy ) ) { return; } header( sprintf( 'Permissions-Policy: %s', $policy ), false ); } /** * Build the Permissions-Policy header value based on active settings. * * @return string Comma-separated policy directives, or empty string when none apply. */ public function get_permissions_policy(): string { $parts = []; $service_url = esc_url( Optml_Config::$service_url ); if ( $this->settings->is_scale_enabled() ) { $parts[] = sprintf( 'ch-viewport-width=(self "%s")', $service_url ); } if ( $this->settings->get( 'network_optimization' ) === 'enabled' ) { $parts[] = sprintf( 'ch-ect=(self "%s")', $service_url ); } if ( $this->settings->get( 'retina_images' ) === 'enabled' ) { $parts[] = sprintf( 'ch-dpr=(self "%s")', $service_url ); } return implode( ', ', $parts ); } /** * Function that purges the image cache for a specific file. * * @param string $file Path or url of the file to clear. * * @return void */ public function purge_image_cache( $file ) { $basename = wp_basename( $file ); $settings = new Optml_Settings(); $settings->clear_cache( $basename ); } /** * Listen when the file is updated and clear the cache for the file. * * @param string $file The file path. * @param int $post_id The post ID. * * @return string The file path. */ public function listen_update_file( $file, $post_id ) { // Purge the image cache. do_action( 'optml_purge_image_cache', $file ); return $file; } /** * Check if the file is an SVG, if so handle appropriately * * @param array $file An array of data for a single file. * * @return mixed */ public function check_svg_and_sanitize( $file ) { // Ensure we have a proper file path before processing. if ( ! isset( $file['tmp_name'] ) ) { return $file; } $file_name = isset( $file['name'] ) ? $file['name'] : ''; $wp_filetype = wp_check_filetype_and_ext( $file['tmp_name'], $file_name ); $type = ! empty( $wp_filetype['type'] ) ? $wp_filetype['type'] : ''; if ( 'image/svg+xml' === $type ) { if ( ! current_user_can( 'upload_files' ) ) { $file['error'] = 'Invalid'; return $file; } if ( ! $this->sanitize_svg( $file['tmp_name'] ) ) { $file['error'] = 'Invalid'; } } return $file; } /** * Sanitize the SVG * * @param string $file Temp file path. * * @return bool|int */ protected function sanitize_svg( $file ) { // We can ignore the phpcs warning here as we're reading and writing to the Temp file. $dirty = file_get_contents( $file ); // phpcs:ignore // Is the SVG gzipped? If so we try and decode the string. $is_zipped = $this->is_gzipped( $dirty ); if ( $is_zipped && ( ! function_exists( 'gzdecode' ) || ! function_exists( 'gzencode' ) ) ) { return false; } if ( $is_zipped ) { $dirty = gzdecode( $dirty ); // If decoding fails, bail as we're not secure. if ( false === $dirty ) { return false; } } $sanitizer = new Sanitizer(); $clean = $sanitizer->sanitize( $dirty ); if ( false === $clean ) { return false; } // If we were gzipped, we need to re-zip. if ( $is_zipped ) { $clean = gzencode( $clean ); } // We can ignore the phpcs warning here as we're reading and writing to the Temp file. file_put_contents( $file, $clean ); // phpcs:ignore return true; } /** * Check if the contents are gzipped * * @see http://www.gzip.org/zlib/rfc-gzip.html#member-format * * @param string $contents Content to check. * * @return bool */ protected function is_gzipped( $contents ) { // phpcs:disable Generic.Strings.UnnecessaryStringConcat.Found if ( function_exists( 'mb_strpos' ) ) { return 0 === mb_strpos( $contents, "\x1f" . "\x8b" . "\x08" ); } else { return 0 === strpos( $contents, "\x1f" . "\x8b" . "\x08" ); } // phpcs:enable } /** * Query the database for images and extract the alt/title to send them to the API * * @uses action: optml_pull_image_data */ public function pull_image_data() { // Get all image attachments that are not processed $args = [ 'post_type' => 'attachment', 'post_mime_type' => 'image', 'posts_per_page' => self::IMAGE_DATA_COLLECTED_BATCH, 'meta_query' => [ 'relation' => 'AND', [ 'key' => self::IMAGE_DATA_COLLECTED, 'compare' => 'NOT EXISTS', ], ], ]; $attachments = get_posts( $args ); $image_data = []; foreach ( $attachments as $attachment ) { // Get image URL, alt, and title $image_url = wp_get_attachment_url( $attachment->ID ); $image_alt = get_post_meta( $attachment->ID, '_wp_attachment_image_alt', true ); $image_title = $attachment->post_title; $image_data[ $image_url ] = []; if ( ! empty( $image_alt ) ) { $image_data[ $image_url ]['alt'] = $image_alt; } if ( ! empty( $image_title ) ) { $image_data[ $image_url ]['title'] = $image_title; } if ( empty( $image_data[ $image_url ] ) ) { unset( $image_data[ $image_url ] ); } // Mark the image as processed update_post_meta( $attachment->ID, self::IMAGE_DATA_COLLECTED, 'yes' ); } if ( ! empty( $image_data ) ) { $api = new Optml_Api(); $api->call_data_enrich_api( $image_data ); } } /** * Delete the processed meta from an image when the alt text is changed * * @uses action: updated_post_meta, added_post_meta */ public function detect_image_alt_change( $meta_id, $post_id, $meta_key, $meta_value ) { // Check if the updated metadata is for alt text and it's an attachment if ( $meta_key === '_wp_attachment_image_alt' && get_post_type( $post_id ) === 'attachment' ) { delete_post_meta( $post_id, self::IMAGE_DATA_COLLECTED ); } } /** * Delete the processed meta from an image when the title is changed * * @uses filter: wp_insert_attachment_data */ public function detect_image_title_changes( $data, $postarr, $unsanitized_postarr, $update ) { // Check if it's an attachment being updated if ( $data['post_type'] !== 'attachment' ) { return $data; } if ( ! isset( $postarr['ID'] ) ) { return $data; } if ( isset( $postarr['post_title'] ) && isset( $postarr['original_post_title'] ) && $postarr['post_title'] !== $postarr['original_post_title'] ) { delete_post_meta( $postarr['ID'], self::IMAGE_DATA_COLLECTED ); } return $data; } /** * Delete the processed meta from an image when the title is changed * * @uses filter: wp_insert_attachment_data */ public function legacy_detect_image_title_changes( $data, $postarr, $unsanitized_postarr ) { return $this->detect_image_title_changes( $data, $postarr, $unsanitized_postarr, true ); } /** * Init no_script setup value based on whether the user is connected or not. */ public function init_no_script() { if ( $this->settings->is_connected() ) { $raw_settings = $this->settings->get_raw_settings(); if ( ! isset( $raw_settings['no_script'] ) ) { $this->settings->update( 'no_script', 'enabled' ); } } } /** * Checks if domain has changed */ public function check_domain_change() { $previous_domain = get_option( 'optml_current_domain', 0 ); $site_url = $this->to_domain_hash( get_home_url() ); if ( $site_url !== $previous_domain ) { update_option( 'optml_current_domain', $site_url ); if ( $previous_domain !== 0 ) { $this->daily_sync(); } } } /** * Update the limit dimensions setting to enabled if the user is new. */ public function update_default_settings() { if ( get_option( self::NEW_USER_DEFAULTS_UPDATED ) === 'yes' ) { return; } if ( $this->settings->is_connected() ) { update_option( self::NEW_USER_DEFAULTS_UPDATED, 'yes' ); return; } $this->settings->update( 'limit_dimensions', 'enabled' ); $this->settings->update( 'lazyload', 'enabled' ); $this->settings->update( 'skip_lazyload_images', '2' ); $this->settings->update( 'compression_mode', 'speed_optimized' ); update_option( self::NEW_USER_DEFAULTS_UPDATED, 'yes' ); } /** * Enable limit dimensions for old users after removing the Resize option. * * @return void */ public function update_limit_dimensions() { if ( get_option( self::OLD_USER_ENABLED_LD ) === 'yes' ) { return; } // New users already have this enabled as we changed defaults. if ( ! $this->settings->is_connected() ) { return; } $this->settings->update( 'limit_dimensions', 'enabled' ); update_option( self::OLD_USER_ENABLED_LD, 'yes' ); } /** * Enable cloud sites and add the current site to the whitelist. * * @return void */ public function update_cloud_sites_default() { if ( get_option( self::OLD_USER_ENABLED_CL ) === 'yes' ) { return; } if ( ! $this->settings->is_connected() ) { return; } // This is already enabled. Don't alter it. if ( $this->settings->get( 'cloud_images' ) === 'enabled' ) { update_option( self::OLD_USER_ENABLED_CL, 'yes' ); return; } $this->settings->update( 'cloud_images', 'enabled' ); $this->settings->update( 'cloud_sites', $this->settings->get_cloud_sites_whitelist_default() ); update_option( self::OLD_USER_ENABLED_CL, 'yes' ); } /** * Register public actions. */ public function register_public_actions() { add_action( 'wp_head', [ $this, 'generator' ] ); add_filter( 'wp_resource_hints', [ $this, 'add_dns_prefetch' ], 10, 2 ); if ( Optml_Manager::should_ignore_image_tags() ) { return; } if ( ! $this->settings->use_lazyload() || ( $this->settings->get( 'native_lazyload' ) === 'enabled' && $this->settings->get( 'video_lazyload' ) === 'disabled' && $this->settings->get( 'bg_replacer' ) === 'disabled' ) ) { return; } add_action( 'wp_enqueue_scripts', [ $this, 'frontend_scripts' ] ); add_action( 'wp_head', [ $this, 'inline_bootstrap_script' ] ); add_filter( 'optml_additional_html_classes', [ $this, 'add_no_js_class_to_html_tag' ], 10 ); } /** * Use filter to add additional class to html tag. * * @param array $classes The classes to be added. * * @return array */ public function add_no_js_class_to_html_tag( $classes ) { // we need the space padding since some plugins might not target correctly with js there own classes // this causes some issues if they concat directly, this way we can protect against that since no matter what // there will be an extra space padding so that classes can be easily identified return array_merge( $classes, [ ' optml_no_js ' ] ); } /** * Adds script for lazyload/js replacement. */ public function inline_bootstrap_script() { $domain = 'https://' . $this->settings->get_cdn_url() . '/js-lib'; if ( defined( 'OPTML_JS_CDN' ) && constant( 'OPTML_JS_CDN' ) ) { $domain = 'https://' . constant( 'OPTML_JS_CDN' ) . '/js-lib'; } $min = ! OPTML_DEBUG ? '.min' : ''; $bgclasses = Optml_Lazyload_Replacer::get_lazyload_bg_classes(); $watcher_classes = Optml_Lazyload_Replacer::get_watcher_lz_classes(); $lazyload_bg_selectors = Optml_Lazyload_Replacer::get_background_lazyload_selectors(); foreach ( $bgclasses as $key ) { $lazyload_bg_selectors[] = '.' . $key; } $lazyload_bg_selectors = empty( $lazyload_bg_selectors ) ? '' : sprintf( '%s', implode( ', ', (array) $lazyload_bg_selectors ) ); $bgclasses = empty( $bgclasses ) ? '' : sprintf( '"%s"', implode( '","', (array) $bgclasses ) ); $watcher_classes = empty( $watcher_classes ) ? '' : sprintf( '"%s"', implode( '","', (array) $watcher_classes ) ); $default_network = ( $this->settings->get( 'network_optimization' ) === 'enabled' ); $limit_dimensions = $this->settings->get( 'limit_dimensions' ) === 'enabled'; $limit_width = $limit_dimensions ? $this->settings->get( 'limit_width' ) : 0; $limit_height = $limit_dimensions ? $this->settings->get( 'limit_height' ) : 0; $retina_ready = ( $this->settings->get( 'retina_images' ) !== 'enabled' ); $scale_is_disabled = ( $this->settings->get( 'scale' ) === 'enabled' ); $native_lazy_enabled = ( $this->settings->get( 'native_lazyload' ) === 'enabled' ); $output = sprintf( ' ', Optml_Lazyload_Replacer::IFRAME_TEMP_COMMENT, esc_url( $domain ), $min, wp_strip_all_tags( $bgclasses ), $native_lazy_enabled ? 'true' : 'false', $scale_is_disabled ? 'true' : 'false', wp_strip_all_tags( $watcher_classes ), addcslashes( wp_strip_all_tags( $lazyload_bg_selectors ), '"' ), defined( 'OPTML_NETWORK_ON' ) && constant( 'OPTML_NETWORK_ON' ) ? ( OPTML_NETWORK_ON ? 'true' : 'false' ) : ( $default_network ? 'true' : 'false' ), $retina_ready ? 'true' : 'false', $this->settings->get_numeric_quality(), $limit_width, $limit_height ); echo $output; } /** * Add settings links in the plugin listing page. * * @param string[]|mixed $links Old plugin links. * * @return string[]|mixed Altered links. */ public function add_action_links( $links ) { if ( ! is_array( $links ) ) { return $links; } return array_merge( $links, [ '' . __( 'Settings', 'optimole-wp' ) . '', ] ); } /** * Check if we should show the notice. * * @return bool Should show? */ public function should_show_notice() { if ( wp_doing_ajax() ) { return false; } if ( is_network_admin() ) { return false; } if ( ! current_user_can( 'manage_options' ) ) { return false; } if ( $this->settings->is_connected() ) { return false; } $current_screen = get_current_screen(); if ( empty( $current_screen ) ) { return false; } if ( ( get_option( 'optml_notice_optin', 'no' ) === 'yes' ) ) { return false; } return true; } /** * Show upgrade notice. */ public function add_notice_upgrade() { if ( ! $this->should_show_upgrade() ) { return; } ?>
'>

' . number_format_i18n( 2000 ) . '' ); ?>

settings->is_connected() || empty( $current_screen ) ) { return false; } if ( get_option( 'optml_notice_hide_upg', 'no' ) === 'yes' ) { return false; } if ( $current_screen->base !== 'upload' ) { return false; } $service_data = $this->settings->get( 'service_data' ); if ( ! isset( $service_data['plan'] ) ) { return false; } if ( $service_data['plan'] !== 'free' ) { return false; } $visitors_limit = isset( $service_data['visitors_limit'] ) ? (int) $service_data['visitors_limit'] : 0; $visitors_left = isset( $service_data['visitors_left'] ) ? (int) $service_data['visitors_left'] : 0; if ( $visitors_limit === 0 ) { return false; } if ( $visitors_left > 300 ) { return false; } return true; } /** * Build upgrade URL with current user's encoded email. * * @return string Upgrade URL. */ public static function get_upgrade_base_link() { $base_url = 'https://optimole.com/upgrade'; $base_url = tsdk_translate_link( $base_url ); $email = self::get_account_email(); if ( ! empty( $email ) ) { $base_url = add_query_arg( 'email', base64_encode( $email ), $base_url ); } return $base_url; } /** * Get contact URL. * * @return string Contact URL. */ public static function get_contact_base_link() { return tsdk_translate_link( 'https://optimole.com/contact' ); } /** * Retrieve Optimole account email. * * @return string */ private static function get_account_email() { $settings = new Optml_Settings(); $service_data = $settings->get( 'service_data' ); if ( isset( $service_data['user_email'] ) && is_string( $service_data['user_email'] ) ) { $email = sanitize_email( $service_data['user_email'] ); if ( ! empty( $email ) ) { return $email; } } return ''; } /** * CSS styles for Notice. */ public static function notice_styles() { ?> should_show_notice() ) { return; } self::notice_styles(); ?>
<?php echo esc_attr__( 'Logo', 'optimole-wp' ); ?>

', '', '', '' ); ?>

conflicting_plugins->should_show_notice() ) { return; } $plugins = $this->conflicting_plugins->get_conflicting_plugins(); $names = []; foreach ( $plugins as $plugin ) { $plugin_data = get_plugin_data( WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . $plugin ); $names[] = $plugin_data['Name']; } $names = implode( ', ', $names ); self::notice_styles(); self::notice_js( 'optml_dismiss_conflict_notice' ); ?>
<?php echo esc_attr__( 'Logo', 'optimole-wp' ); ?>

', '' ); ?>

get_background_lazy_css(); wp_register_style( 'optm_lazyload_noscript_style', false ); wp_enqueue_style( 'optm_lazyload_noscript_style' ); wp_add_inline_style( 'optm_lazyload_noscript_style', "html.optml_no_js img[data-opt-src] { display: none !important; } \n " . $bg_css ); if ( $this->settings->use_lazyload() === true ) { wp_register_script( 'optml-print', false ); wp_enqueue_script( 'optml-print' ); $script = ' (function(w, d){ w.addEventListener("beforeprint", function(){ let images = d.getElementsByTagName( "img" ); for (let img of images) { if ( !img.dataset.optSrc) { continue; } img.src = img.dataset.optSrc; delete img.dataset.optSrc; } }); }(window, document)); '; wp_add_inline_script( 'optml-print', $script ); } if ( Optml_Manager::should_load_profiler() ) { add_action( 'wp_footer', function () { print ( self::get_optimizer_script() ); } ); } print ( self::get_preload_links_placeholder() ); } /** * Get the optimizer script. * * @param bool $placeholder Whether to return the placeholder or the actual script. * * @return string The optimizer script. */ public static function get_optimizer_script( $placeholder = true ) { if ( $placeholder ) { return ''; } return ''; } /** * Get the preload links placeholder. * * @return string The preload links placeholder. */ public static function get_preload_links_placeholder(): string { return ''; } /** * Maybe redirect to dashboard page. */ public function maybe_redirect() { if ( isset( $_GET['optml_nonce'] ) && isset( $_GET['optml_hide_optin'] ) && $_GET['optml_hide_optin'] === 'yes' && wp_verify_nonce( $_GET['optml_nonce'], 'hide_nonce' ) ) { update_option( 'optml_notice_optin', 'yes' ); } if ( isset( $_GET['optml_nonce'] ) && isset( $_GET['optml_hide_upg'] ) && $_GET['optml_hide_upg'] === 'yes' && wp_verify_nonce( $_GET['optml_nonce'], 'hide_nonce' ) ) { update_option( 'optml_notice_hide_upg', 'yes' ); } if ( ! get_transient( 'optml_fresh_install' ) ) { return; } if ( wp_doing_ajax() ) { return; } delete_transient( 'optml_fresh_install' ); if ( is_network_admin() || isset( $_GET['activate-multi'] ) ) { return; } if ( $this->settings->is_connected() ) { return; } wp_safe_redirect( admin_url( 'admin.php?page=optimole' ) ); exit; } /** * Output Generator tag. */ public function generator() { if ( ! $this->settings->is_connected() ) { return; } if ( ! $this->settings->is_enabled() ) { return; } $hints = $this->get_accept_ch_hints(); if ( empty( $hints ) ) { return; } echo sprintf( '', esc_attr( $hints ) ); } /** * Build the Accept-CH meta content value based on active settings. * * Mirrors the directives used in get_permissions_policy() so both * the Permissions-Policy header and the Accept-CH meta stay in sync. * * @return string Comma-separated hint tokens, or empty string when none apply. */ public function get_accept_ch_hints(): string { $hints = []; if ( $this->settings->is_scale_enabled() ) { $hints[] = 'Viewport-Width'; } if ( $this->settings->get( 'network_optimization' ) === 'enabled' ) { $hints[] = 'ECT'; } if ( $this->settings->get( 'retina_images' ) === 'enabled' ) { $hints[] = 'DPR'; } return implode( ', ', $hints ); } /** * Update daily the quota routine. */ public function daily_sync() { $api_key = $this->settings->get( 'api_key' ); $service_data = $this->settings->get( 'service_data' ); $application = ''; if ( isset( $service_data['cdn_key'] ) ) { $application = $service_data['cdn_key']; } if ( empty( $api_key ) ) { return; } $request = new Optml_Api(); $data = $request->get_user_data( $api_key, $application ); if ( $data === false || is_wp_error( $data ) ) { return; } if ( $data === 'disconnect' ) { $settings = new Optml_Settings(); $settings->reset(); return; } add_filter( 'optml_dont_trigger_settings_updated', '__return_true' ); $this->settings->update( 'service_data', $data ); if ( isset( $data['extra_visits'] ) ) { $this->settings->update_frontend_banner_from_remote( $data['extra_visits'] ); } // Here the account got deactivated, in this case we check if the user is using offloaded images and we roll them back. $should_revert_offloading = isset( $data['status'] ) && $data['status'] === 'inactive'; // The user is now on a plan without offloading and the grace period is over. if ( isset( $data['should_revert_offload'] ) && (bool) $data['should_revert_offload'] ) { $should_revert_offloading = true; } if ( $should_revert_offloading ) { // We check if the user has images offloaded. if ( $this->settings->get( 'offload_media' ) === 'disabled' ) { return; } $in_progress = $this->settings->get( 'offloading_status' ) !== 'disabled'; // We check if there is an in progress transfer, we stop it. if ( $in_progress ) { $this->settings->update( 'offloading_status', 'disabled' ); } $this->settings->update( 'rollback_status', 'enabled' ); // We start the rollback process. Optml_Logger::instance()->add_log( 'rollback_images', 'Account deactivated, starting rollback.' ); Optml_Media_Offload::move_images( 'rollback_images', false ); $this->settings->update( 'offload_media', 'disabled' ); } remove_filter( 'optml_dont_trigger_settings_updated', '__return_true' ); } /** * Adds cdn url for prefetch. * * @param array $hints Hints array. * @param string $relation_type Type of relation. * * @return array Altered hints array. */ public function add_dns_prefetch( $hints, $relation_type ) { if ( 'dns-prefetch' !== $relation_type && 'preconnect' !== $relation_type ) { return $hints; } if ( ! $this->settings->is_connected() ) { return $hints; } if ( ! $this->settings->is_enabled() ) { return $hints; } $hints[] = sprintf( 'https://%s', $this->settings->get_cdn_url() ); return $hints; } /** * Add the dashboard page. */ public function add_dashboard_page() { if ( defined( 'OPTIOMLE_HIDE_ADMIN_AREA' ) && OPTIOMLE_HIDE_ADMIN_AREA ) { return; } add_menu_page( 'Optimole', 'Optimole', 'manage_options', 'optimole', [ $this, 'render_dashboard_page' ], OPTML_URL . 'assets/img/logo.svg', 11 ); } /** * Add menu icon style. * * @return void */ public function menu_icon_style() { $conflicts_count = $this->get_active_notices_count(); $badge_html = ''; if ( $conflicts_count > 0 ) { $badge_html = sprintf( '', $conflicts_count ); } echo '' . $badge_html; } /** * Add the settings page. * * We do it with another priority as it goes above the cloud library otherwise. */ public function add_settings_subpage() { if ( defined( 'OPTIOMLE_HIDE_ADMIN_AREA' ) && OPTIOMLE_HIDE_ADMIN_AREA ) { return; } if ( ! $this->settings->is_connected() ) { return; } add_submenu_page( 'optimole', __( 'Settings', 'optimole-wp' ), __( 'Settings', 'optimole-wp' ), 'manage_options', 'optimole#settings', [ $this, 'render_dashboard_page' ] ); } /** * Redirect old dashboard. * * @return void */ public function redirect_old_dashboard() { if ( ! is_admin() ) { return; } global $pagenow; if ( $pagenow !== 'upload.php' ) { return; } if ( ! isset( $_GET['page'] ) ) { return; } if ( $_GET['page'] !== 'optimole' ) { return; } wp_safe_redirect( admin_url( 'admin.php?page=optimole' ) ); exit; } /** * Render dashboard page. */ public function render_dashboard_page() { if ( get_option( 'optml_notice_optin', 'no' ) !== 'yes' ) { update_option( 'optml_notice_optin', 'yes' ); } ?>
id ) ) { return; } if ( $current_screen->id === 'upload' ) { wp_enqueue_script( OPTML_NAMESPACE . '-media-admin', OPTML_URL . 'assets/js/media.js', [], OPTML_VERSION ); wp_localize_script( OPTML_NAMESPACE . '-media-admin', 'optimoleMediaListing', [ 'rest_url' => rest_url( OPTML_NAMESPACE . '/v1/move_image' ), 'nonce' => wp_create_nonce( 'wp_rest' ), ] ); wp_register_style( 'optml_media_admin_style', false ); wp_add_inline_style( 'optml_media_admin_style', ' .is-loading { pointer-events: none; opacity: 0.5; display: inline-block; } .spinner { margin-top: 0px; } ' ); wp_enqueue_style( 'optml_media_admin_style' ); } if ( $current_screen->id !== 'toplevel_page_optimole' ) { return; } $asset_file = include OPTML_PATH . 'assets/build/dashboard/index.asset.php'; wp_register_script( OPTML_NAMESPACE . '-admin', OPTML_URL . 'assets/build/dashboard/index.js', array_merge( $asset_file['dependencies'], [ 'updates' ] ), $asset_file['version'], true ); wp_localize_script( OPTML_NAMESPACE . '-admin', 'optimoleDashboardApp', $this->localize_dashboard_app() ); wp_enqueue_script( OPTML_NAMESPACE . '-admin' ); wp_enqueue_style( OPTML_NAMESPACE . '-admin', OPTML_URL . 'assets/build/dashboard/style-index.css', [ 'wp-components', ], $asset_file['version'] ); do_action( 'themeisle_internal_page', OPTML_PRODUCT_SLUG, 'dashboard' ); } /** * Localize the dashboard app. * * @codeCoverageIgnore * @return array */ private function localize_dashboard_app() { global $wp_version; $is_offload_media_available = 'no'; if ( version_compare( $wp_version, '5.3', '>=' ) ) { $is_offload_media_available = 'yes'; } $api_key = $this->settings->get( 'api_key' ); $service_data = $this->settings->get( 'service_data' ); if ( ! is_array( $service_data ) ) { $service_data = []; } $user = get_userdata( get_current_user_id() ); $user_status = 'inactive'; $auto_connect = get_option( Optml_Settings::OPTML_USER_EMAIL, 'no' ); $available_apps = isset( $service_data['available_apps'] ) ? $service_data['available_apps'] : null; if ( isset( $service_data['cdn_key'] ) && $available_apps !== null ) { foreach ( $service_data['available_apps'] as $app ) { if ( isset( $app['key'] ) && $app['key'] === $service_data['cdn_key'] && isset( $app['status'] ) && $app['status'] === 'active' ) { $user_status = 'active'; } } } $routes = []; foreach ( Optml_Rest::$rest_routes as $route_type ) { foreach ( $route_type as $route => $details ) { $routes[ $route ] = OPTML_NAMESPACE . '/v1/' . $route; } } // Disable this for now, we need a better way to detect it. // $cron_disabled = apply_filters( 'optml_offload_wp_cron_disabled', defined( 'DISABLE_WP_CRON' ) && constant( 'DISABLE_WP_CRON' ) === true ); $language = get_user_locale(); $available_languages = [ 'de_DE' => 'de', 'de_DE_formal' => 'de', ]; $lang_code = isset( $available_languages[ $language ] ) ? 'de' : 'en'; $site_settings = $this->settings->get_site_settings(); if ( ! empty( $service_data ) ) { $service_data['domain_dns'] = $this->settings->get_cdn_url(); } if ( isset( $service_data['renews_on'] ) ) { $service_data['renews_on_formatted'] = date_i18n( get_option( 'date_format' ), $service_data['renews_on'] ); } $user_plan = isset( $service_data['plan'] ) ? $service_data['plan'] : 'free'; return [ 'strings' => $this->get_dashboard_strings(), 'assets_url' => OPTML_URL . 'assets/', 'dam_url' => 'admin.php?page=optimole-dam', 'connection_status' => empty( $service_data ) ? 'no' : 'yes', 'has_application' => isset( $service_data['app_count'] ) && $service_data['app_count'] >= 1 ? 'yes' : 'no', 'user_status' => $user_status, 'available_apps' => $available_apps, 'api_key' => $api_key, 'routes' => $routes, 'language' => $lang_code, 'nonce' => wp_create_nonce( 'wp_rest' ), 'user_data' => $service_data, 'remove_latest_images' => defined( 'OPTML_REMOVE_LATEST_IMAGES' ) && constant( 'OPTML_REMOVE_LATEST_IMAGES' ) ? ( OPTML_REMOVE_LATEST_IMAGES ? 'yes' : 'no' ) : 'no', 'current_user' => [ 'email' => $user->user_email, ], 'site_settings' => $site_settings, 'offload_limit' => $this->settings->get( 'offload_limit' ), 'home_url' => home_url(), 'optimoleHome' => tsdk_translate_link( 'https://optimole.com/' ), 'optimoleDashHome' => tsdk_translate_link( 'https://dashboard.optimole.com/', 'query' ), 'optimoleDashBilling' => tsdk_translate_link( 'https://dashboard.optimole.com/settings/billing', 'query' ), 'optimoleDashMetrics' => tsdk_translate_link( tsdk_utmify( 'https://dashboard.optimole.com/metrics', 'wp-plugin', 'shortcut' ) ), 'offload_upgrade_url' => tsdk_translate_link( tsdk_utmify( 'https://optimole.com/pricing/', 'offload' ) ), 'days_since_install' => round( ( time() - get_option( 'optimole_wp_install', 0 ) ) / DAY_IN_SECONDS ), 'is_offload_media_available' => $is_offload_media_available, 'auto_connect' => $auto_connect, 'cron_disabled' => false, // $cron_disabled && ! function_exists( 'as_schedule_single_action' ), 'submenu_links' => [ [ 'href' => 'admin.php?page=optimole#settings', 'text' => __( 'Settings', 'optimole-wp' ), 'hash' => '#settings', ], ], 'bf_notices' => $this->get_bf_notices( $user_plan ), 'spc_banner' => $this->get_spc_banner(), 'show_exceed_plan_quota_notice' => $this->should_show_exceed_quota_warning(), 'show_free_user_with_offload_notice' => get_option( 'optml_has_offloading_enabled_on_upgrade', 'no' ), 'report_issue_url' => add_query_arg( [ 'utm_source' => 'plugin', 'utm_medium' => 'wp_dashboard', 'utm_campaign' => 'report_issue', 'contact_website' => home_url(), ], self::get_contact_base_link() ), 'upgrade_url' => esc_url( self::get_upgrade_base_link() ), ]; } /** * Get the SPC banner data. * * @return array|null */ private function get_spc_banner() { // User can't dismiss notice or install plugins. if ( ! current_user_can( 'manage_options' ) || ! current_user_can( 'install_plugins' ) ) { return null; } // User has dismissed the notice. if ( get_option( self::SPC_BANNER_DISMISS_KEY ) === 'yes' ) { return null; } // User has installed the pro plugin. if ( defined( 'SPC_PRO_PATH' ) ) { return null; } // User has installed the plugin. if ( defined( 'SPC_PATH' ) ) { return null; } return [ 'activate_url' => $this->get_spc_activate_url(), 'status' => $this->get_spc_status(), 'banner_dismiss_key' => self::SPC_BANNER_DISMISS_KEY, 'i18n' => [ 'dismiss' => __( 'Dismiss', 'optimole-wp' ), 'title' => __( 'Pair Optimole with Super Page Cache', 'optimole-wp' ), 'byline' => __( 'Improve your Core Web Vitals with our recommended caching solution', 'optimole-wp' ), 'features' => [ __( 'Edge Caching (with Cloudflare free plan)', 'optimole-wp' ), __( 'Works with Optimole optimization', 'optimole-wp' ), __( 'Lightning Speed Disk Caching', 'optimole-wp' ), ], 'cta' => __( 'Get Super Page Cache', 'optimole-wp' ), 'activate' => __( 'Activate Super Page Cache', 'optimole-wp' ), 'installing' => __( 'Installing Super Page Cache...', 'optimole-wp' ), 'activating' => __( 'Activating Super Page Cache...', 'optimole-wp' ), 'activated' => __( 'Super Page Cache is active!', 'optimole-wp' ), 'error' => __( 'Something went wrong. Please refresh the page and try again.', 'optimole-wp' ), ], ]; } /** * Get the SPC activate URL. * * @return string */ private function get_spc_activate_url() { return add_query_arg( [ 'plugin_status' => 'all', 'paged' => 1, 'action' => 'activate', 'plugin' => rawurlencode( 'wp-cloudflare-page-cache/wp-cloudflare-super-page-cache.php' ), '_wpnonce' => wp_create_nonce( 'activate-plugin_wp-cloudflare-page-cache/wp-cloudflare-super-page-cache.php' ), ], admin_url( 'plugins.php' ) ); } /** * Get the SPC status. * * @return string */ private function get_spc_status() { if ( is_plugin_active( 'wp-cloudflare-page-cache/wp-cloudflare-super-page-cache.php' ) ) { return 'active'; } if ( file_exists( WP_PLUGIN_DIR . '/wp-cloudflare-page-cache/wp-cloudflare-super-page-cache.php' ) ) { return 'installed'; } return 'not-installed'; } /** * Get the black friday notices. * * @param non-empty-string $user_plan User plan. * * @return array Notice data. */ public function get_bf_notices( $user_plan ) { $notices = []; $has_free_plan = 'free' === $user_plan; if ( ! $has_free_plan ) { return $notices; } /** * The black friday sale period flag. * * @var bool $is_black_friday Whether is black friday sale period or not. */ $is_black_friday = apply_filters( 'themeisle_sdk_is_black_friday_sale', false ); if ( ! $is_black_friday ) { return $notices; } /** * The current date. * * @var DateTime $now Current date. */ $now = apply_filters( 'themeisle_sdk_current_date', new \DateTime( 'now' ) ); $current_year = $now->format( 'Y' ); $black_friday_day = new \DateTime( "last Friday of November $current_year" ); $sale_start = clone $black_friday_day; $sale_start->modify( 'monday this week' ); $sale_start->setTime( 0, 0 ); $end = clone $sale_start; $end->modify( '+7 days' ); $end->setTime( 23, 59, 59 ); $promo_code = 'BFCM2525'; $notices['sidebar'] = [ 'title' => sprintf( '%1$s: %2$s - %3$s', __( 'Private Sale', 'optimole-wp' ), wp_date( 'j M', $sale_start->getTimestamp() ), wp_date( 'j M', $end->getTimestamp() ) ), 'subtitle' => sprintf( /* translators: %1$s is the promo code, %2$s is the discount amount ('25 off') */ __( 'Use code %1$s for %2$s on yearly plans.', 'optimole-wp' ), '' . $promo_code . '', '' . __( '25% discount', 'optimole-wp' ) . '' ), 'cta_link' => esc_url_raw( tsdk_utmify( tsdk_translate_link( self::get_upgrade_base_link() ), 'bfcm25', 'sidebarnotice' ) ), ]; if ( get_option( self::BF_PROMO_DISMISS_KEY ) === 'yes' ) { return $notices; } $message = sprintf( // translators: %1$s is the promo code, %2$s is the discount amount ('25% off') __( 'Use coupon code %1$s for an instant %2$s on Optimole yearly plans', 'optimole-wp' ), '' . $promo_code . '', '' . __( '25% discount', 'optimole-wp' ) . '' ); $cta_link = esc_url_raw( tsdk_utmify( self::get_upgrade_base_link(), 'bfcm25', 'notice' ) ); $cta_text = __( 'Claim now', 'optimole-wp' ); $notices['banner'] = [ /* translators: number of days left */ 'urgency' => sprintf( __( 'Hurry up! only %s left', 'optimole-wp' ), human_time_diff( $end->getTimestamp(), $now->getTimestamp() ) ), /* translators: private sale */ 'title' => sprintf( __( 'Black Friday %s', 'optimole-wp' ), '' . __( 'private sale', 'optimole-wp' ) . '' ), 'subtitle' => $message, 'cta_text' => $cta_text, 'cta_link' => $cta_link, 'dismiss_key' => self::BF_PROMO_DISMISS_KEY, ]; return $notices; } /** * Get all dashboard strings. * * @codeCoverageIgnore * @return array */ private function get_dashboard_strings() { return [ 'optimole' => 'Optimole', 'version' => OPTML_VERSION, 'terms_menu' => __( 'Terms', 'optimole-wp' ), 'privacy_menu' => __( 'Privacy', 'optimole-wp' ), 'testdrive_menu' => __( 'Test Optimole', 'optimole-wp' ), 'service_details' => __( 'Image optimization service', 'optimole-wp' ), 'dashboard_title' => __( 'Image Optimization Overview', 'optimole-wp' ), 'banner_title' => __( 'All images are automatically optimized!', 'optimole-wp' ), 'banner_description' => __( 'Optimole is handling all your images in real-time with our CloudFront CDN (450+ locations worldwide)', 'optimole-wp' ), 'quick_action_title' => __( 'Quick Actions', 'optimole-wp' ), 'connect_btn' => __( 'Connect to Optimole', 'optimole-wp' ), 'disconnect_btn' => __( 'Disconnect', 'optimole-wp' ), 'select' => __( 'Select', 'optimole-wp' ), 'your_domain' => __( 'your domain', 'optimole-wp' ), 'add_api' => __( 'Add your API Key', 'optimole-wp' ), 'your_api_key' => __( 'Your API Key', 'optimole-wp' ), 'looking_for_api_key' => __( 'LOOKING FOR YOUR API KEY?', 'optimole-wp' ), 'refresh_stats_cta' => __( 'Refresh Stats', 'optimole-wp' ), 'updating_stats_cta' => __( 'UPDATING STATS', 'optimole-wp' ), 'api_key_placeholder' => __( 'API Key', 'optimole-wp' ), 'account_needed_heading' => __( 'Supercharge Your WordPress Images in 60 Seconds', 'optimole-wp' ), 'account_needed_sub_heading' => __( 'Stop sacrificing image quality for page speed. Optimole delivers both.', 'optimole-wp' ), 'account_needed_trust_badge' => __( 'TRUSTED BY 200,000+ HAPPY USERS', 'optimole-wp' ), 'account_needed_setup_time' => __( 'Setup is instant - just click connect', 'optimole-wp' ), 'account_needed_benefits_toggle' => __( 'See what you\'ll get', 'optimole-wp' ), 'invalid_key' => __( 'Invalid API Key', 'optimole-wp' ), 'keep_connected' => __( 'Ok, keep me connected', 'optimole-wp' ), 'cloud_library' => __( 'Cloud Library', 'optimole-wp' ), 'image_storage' => __( 'Image Storage', 'optimole-wp' ), 'word' => __( 'word', 'optimole-wp' ), 'path' => __( 'path', 'optimole-wp' ), 'disconnect_title' => __( 'You are about to disconnect from the Optimole API', 'optimole-wp' ), 'disconnect_desc' => __( 'Please note that disconnecting your site from the Optimole API will impact your website performance. If you still want to disconnect click the button below.', 'optimole-wp' ), 'email_address_label' => __( 'Your email address', 'optimole-wp' ), 'steps_connect_api_title' => __( 'Connect your account', 'optimole-wp' ), 'register_btn' => __( 'Create & connect your account', 'optimole-wp' ), 'secure_connection' => __( 'Secure connection - your data is protected', 'optimole-wp' ), 'step_one_api_title' => __( 'Enter your API key.', 'optimole-wp' ), 'optml_dashboard' => sprintf( /* translators: 1 is the opening anchor tag, 2 is the closing anchor tag */ __( 'Get it from the %1$s Optimole Dashboard%2$s.', 'optimole-wp' ), ' ', '' ), 'steps_connect_api_desc' => sprintf( /* translators: 1 is the opening anchor tag, 2 is the closing anchor tag */ __( 'Copy the API Key you have received via email or you can get it from %1$s Optimole dashboard%2$s. If your account has multiple domains select the one you want to use.
', 'optimole-wp' ), ' ', '' ), 'api_exists' => __( 'I already have an API key.', 'optimole-wp' ), 'back_to_register' => __( 'Register account', 'optimole-wp' ), 'back_to_connect' => __( 'Go to previous step', 'optimole-wp' ), /* translators: 1 is starting anchor tag, 2 is ending anchor tag */ 'error_register' => sprintf( __( 'Error registering account. You can try again %1$shere%2$s', 'optimole-wp' ), ' ', '' ), 'invalid_email' => __( 'Please use a valid email address.', 'optimole-wp' ), 'connected' => __( 'CONNECTED', 'optimole-wp' ), 'connecting' => __( 'CONNECTING', 'optimole-wp' ), 'not_connected' => __( 'NOT CONNECTED', 'optimole-wp' ), 'usage' => __( 'Monthly Usage', 'optimole-wp' ), 'quota' => __( 'Monthly visits:', 'optimole-wp' ), 'tooltip_visits_title' => __( 'What are visits?', 'optimole-wp' ), /* translators: 1 is the day when the visits reset, for example 1st of each month */ 'tooltip_visits_description' => __( 'Each visitor to your site is counted as a unique daily user, regardless of their actions or return visits on the same day. Your visit count resets on %s.', 'optimole-wp' ), 'logged_in_as' => __( 'LOGGED IN AS', 'optimole-wp' ), 'private_cdn_url' => __( 'IMAGES DOMAIN', 'optimole-wp' ), 'existing_user' => __( 'Existing user?', 'optimole-wp' ), 'notification_message_register' => __( 'We sent you the API Key in the email. Add it below to connect to Optimole.', 'optimole-wp' ), 'premium_support' => __( 'Access our Premium Support', 'optimole-wp' ), 'account_needed_title' => sprintf( /* translators: 1 is the link to optimole.com */ __( 'Connect to Optimole\'s powerful image optimization service with a free API key from %s.', 'optimole-wp' ), ' optimole.com' ), 'account_needed_subtitle_1' => sprintf( /* translators: 1 is the starting bold tag, 2 is the ending bold tag, 3 is the starting bold tag, 4 is the limit number, 5 is ending bold tag, 6 is the starting anchor tag for the docs link on how we count visits, 7 is the ending anchor tag. */ __( '%1$sOptimize unlimited images%2$s for up to %3$s monthly %4$svisitors%5$s - completely FREE.', 'optimole-wp' ), '', '', number_format_i18n( 2000 ), '', '' ), 'account_needed_subtitle_3' => sprintf( /* translators: 1 is the starting anchor tag for the docs link, 2 is the ending anchor tag. */ __( 'Need help? %1$sGetting Started with Optimole%2$s', 'optimole-wp' ), '', '' ), 'account_needed_subtitle_2' => sprintf( /* translators: 1 is the starting bold tag, 2 is the ending bold tag */ __( '%1$sInstant global delivery%2$s with CloudFront CDN - your images load 2-3x faster worldwide from 450+ locations.', 'optimole-wp' ), '', '' ), 'account_needed_subtitle_4' => sprintf( /* translators: 1 is the starting bold tag, 2 is the ending bold tag */ __( '%1$sAdaptive optimization%2$s that perfectly sizes images for every visitor\'s device and connection speed.', 'optimole-wp' ), '', '' ), 'account_needed_footer' => sprintf( /* translators: 1 is the number of users using Optimole */ __( 'Trusted by more than %1$s happy users', 'optimole-wp' ), number_format_i18n( 200000, 0 ) ), 'account_connecting_title' => __( 'Connecting to Optimole', 'optimole-wp' ), 'account_connecting_subtitle' => __( 'Sit tight while we connect you to the Dashboard', 'optimole-wp' ), 'notice_just_activated' => ! $this->settings->is_connected() ? sprintf( /* translators: 1 starting bold tag, 2 is the ending bold tag */ __( '%1$sImage optimisation is currently running.%2$s
Your visitors will now view the best image for their device automatically, all served from the Optimole Cloud Service on the fly. You might see for the very first image request being redirected to the original URL while we do the optimization in the background. You can relax, we\'ll take it from here.', 'optimole-wp' ), '', '' ) : '', 'notice_api_not_working' => __( 'It seems there is an issue with your WordPress configuration and the core REST API functionality is not available. This is crucial as Optimole relies on this functionality in order to work.
The root cause might be either a security plugin which blocks this feature or some faulty server configuration which constrain this WordPress feature.You can try to disable any of the security plugins that you use in order to see if the issue persists or ask the hosting company to further investigate.', 'optimole-wp' ), 'notice_disabled_account' => sprintf( /* translators: 1 anchor tag to pricing, 2 is the ending endin anchor tag, 3 is the bold tag start, 4 ending bold tag, 5 new line tag */ __( '%3$sYour account has been disabled due to exceeding quota.%4$s All images are being redirected to the original unoptimized URL. %5$sPlease %1$supgrade%2$s to re-activate the account.', 'optimole-wp' ), '', '', '', '', '
' ), 'signup_terms' => sprintf( /* translators: 1 is starting anchor tag to terms, 2 is starting anchor tag to privacy link and 3 is ending anchor tag. */ __( 'By signing up, you agree to our %1$sTerms of Service %3$s and %2$sPrivacy Policy %3$s.', 'optimole-wp' ), '', '', '' ), 'dashboard_menu_item' => __( 'Dashboard', 'optimole-wp' ), 'settings_menu_item' => __( 'Settings', 'optimole-wp' ), 'help_menu_item' => __( 'Help', 'optimole-wp' ), 'settings_watermark_menu_item' => __( 'Watermark', 'optimole-wp' ), 'settings_exclusions_menu_item' => __( 'Exclusions', 'optimole-wp' ), 'settings_resize_menu_item' => __( 'Resize', 'optimole-wp' ), 'settings_compression_menu_item' => __( 'Compression', 'optimole-wp' ), 'advanced_settings_menu_item' => __( 'Advanced', 'optimole-wp' ), 'general_settings_menu_item' => __( 'General', 'optimole-wp' ), 'lazyload_settings_menu_item' => __( 'Lazyload', 'optimole-wp' ), 'watermarks_menu_item' => __( 'Watermark', 'optimole-wp' ), 'conflicts_menu_item' => __( 'Possible Issues', 'optimole-wp' ), 'conflicts' => [ 'title' => __( 'We might have some possible conflicts with the plugins that you use. In order to benefit from Optimole\'s full potential you will need to address this issues.', 'optimole-wp' ), 'message' => __( 'Details', 'optimole-wp' ), 'conflict_close' => __( 'I\'ve done this.', 'optimole-wp' ), 'no_conflicts_found' => __( 'No conflicts found. We are all peachy now. 🍑', 'optimole-wp' ), ], 'upgrade' => [ 'title' => __( 'Upgrade', 'optimole-wp' ), 'title_long' => __( 'Upgrade to Optimole Pro', 'optimole-wp' ), 'reason_1' => __( 'Priority & Live Chat support', 'optimole-wp' ), 'reason_2' => __( 'Extend visits limit', 'optimole-wp' ), 'reason_3' => __( 'Custom domain', 'optimole-wp' ), 'reason_4' => __( 'Site audit', 'optimole-wp' ), 'cta' => __( 'View plans', 'optimole-wp' ), ], 'neve' => [ 'is_active' => defined( 'NEVE_VERSION' ) ? 'yes' : 'no', 'byline' => __( 'Fast, perfomance built-in WordPress theme.', 'optimole-wp' ), 'reason_1' => __( 'Lightweight, 25kB in page-weight.', 'optimole-wp' ), 'reason_2' => __( '100+ Starter Sites available.', 'optimole-wp' ), 'reason_3' => __( 'AMP/Mobile ready.', 'optimole-wp' ), 'reason_4' => __( 'Lots of customizations options.', 'optimole-wp' ), 'reason_5' => __( 'Fully compatible with Optimole.', 'optimole-wp' ), ], 'metrics' => [ 'metricsTitle1' => __( 'Images optimized', 'optimole-wp' ), 'metricsSubtitle1' => __( 'Since plugin activation', 'optimole-wp' ), 'metricsTitle2' => __( 'Saved File Size', 'optimole-wp' ), 'metricsSubtitle2' => __( 'Latest 10 images', 'optimole-wp' ), 'metricsTitle3' => __( 'Average Compression', 'optimole-wp' ), 'metricsSubtitle3' => __( 'Average Reduction', 'optimole-wp' ), 'metricsTitle4' => __( 'CDN Traffic', 'optimole-wp' ), 'metricsSubtitle4' => __( 'This month', 'optimole-wp' ), 'metricsTitle5' => __( 'Offloaded images', 'optimole-wp' ), 'metricsSubtitle5' => __( 'Offloaded to cloud', 'optimole-wp' ), // translators: is used as singular when the number of images is 1 for a unit label. 'image' => __( 'image', 'optimole-wp' ), // translators: is used as plural when the number of images is more than 1 for a unit label. 'images' => __( 'images', 'optimole-wp' ), 'view_analytics' => __( 'View Analytics', 'optimole-wp' ), 'adjust_compression' => __( 'Adjust Compression', 'optimole-wp' ), 'manage_offloading' => __( 'Manage image offloading', 'optimole-wp' ), ], 'quick_actions' => [ 'speed_test_title' => __( 'Test Your Site Speed', 'optimole-wp' ), 'speed_test_desc' => __( 'Run speed test', 'optimole-wp' ), 'speed_test_link' => add_query_arg( 'url', get_site_url(), 'https://pagespeed.web.dev/analysis' ), 'clear_cache_images' => __( 'Clear Cached Images', 'optimole-wp' ), 'clear_cache' => __( 'Clear cache', 'optimole-wp' ), 'offload_images' => __( 'Enable Offload Images', 'optimole-wp' ), 'offload_images_desc' => __( 'Free up space on your server', 'optimole-wp' ), 'advance_settings' => __( 'Advanced Settings', 'optimole-wp' ), 'configure_settings' => __( 'Configure settings', 'optimole-wp' ), ], 'options_strings' => [ 'watermark_media_desc' => __( 'Protect your brand by adding a watermark to your images. Configure placement, opacity, and rules directly in your Optimole Dashboard.', 'optimole-wp' ), 'watermark_media_title' => __( 'Watermarks', 'optimole-wp' ), 'learn_more' => __( 'Learn more', 'optimole-wp' ), 'open_optimole_dashboard' => __( 'Open Optimole Dashboard', 'optimole-wp' ), 'watermark_footer' => __( 'Watermark rules and exclusions are fully managed in your Optimole Dashboard.', 'optimole-wp' ), 'watermark_info_1' => __( 'Choose from multiple watermark positions and set opacity.', 'optimole-wp' ), 'watermark_info_2' => __( 'Set different watermarks for different image sizes.', 'optimole-wp' ), 'watermark_info_3' => __( 'Create rule-based targeting by filename, page URL, class, and more.', 'optimole-wp' ), 'compression_mode' => __( 'Compression Mode', 'optimole-wp' ), 'compression_mode_speed_optimized' => __( 'Speed Optimized', 'optimole-wp' ), 'compression_mode_quality_optimized' => __( 'Quality Optimized', 'optimole-wp' ), 'compression_mode_custom' => __( 'Custom', 'optimole-wp' ), 'compression_mode_speed_optimized_desc' => __( 'Prioritizes faster loading and smaller image sizes by applying a balanced level of lossy compression. This setting reduces file size significantly without severely degrading visual quality.', 'optimole-wp' ), 'compression_mode_quality_optimized_desc' => __( 'Delivers high-quality images by applying lossless or near-lossless optimization. Ensures images retain their sharpness and details while still benefiting from moderate file size reduction.', 'optimole-wp' ), 'compression_mode_custom_desc' => __( 'Customize each setting individually for complete control over image compression.', 'optimole-wp' ), 'best_format_title' => __( 'Automatic Best Image Format Selection', 'optimole-wp' ), 'best_format_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'When enabled, Optimole chooses the best format for your images, balancing quality with speed. It tries options like AVIF and WebP to keep visuals sharp and pages fast, though uncached images may take a bit longer to process. %1$sLearn more%2$s.', 'optimole-wp' ), '', '' ), 'add_filter' => __( 'Add filter', 'optimole-wp' ), 'add_site' => __( 'Add site', 'optimole-wp' ), 'admin_bar_desc' => __( 'Show in the WordPress admin bar the available quota from Optimole service.', 'optimole-wp' ), 'auto_q_title' => __( 'Auto', 'optimole-wp' ), 'cache_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Clears all Optimole’s cached resources (images, JS, CSS). Useful if you made changes to your images and don\'t see those applying on your site. %1$sLearn more%2$s', 'optimole-wp' ), '', '' ), 'cache_title' => __( 'Clear Cached Resources', 'optimole-wp' ), 'clear_cache_notice' => __( 'Clearing cached resources will re-optimize the images and might affect the site performance for a few minutes.', 'optimole-wp' ), 'image_size_notice' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Use this option if you notice images are not cropped correctly after using Optimole. Add the affected image sizes here to automatically adjust and correct their cropping for optimal display. %1$sLearn more%2$s', 'optimole-wp' ), '', '' ), 'clear_cache_images' => __( 'Clear cached images', 'optimole-wp' ), 'clear_cache_assets' => __( 'Clear cached CSS & JS', 'optimole-wp' ), 'connect_step_0' => __( 'Connecting your site to the Optimole service.', 'optimole-wp' ), 'connect_step_1' => __( 'Checking for possible conflicts.', 'optimole-wp' ), 'connect_step_2' => __( 'Inspecting the images from your site.', 'optimole-wp' ), 'connect_step_3' => __( 'All done, Optimole is currently optimizing your site.', 'optimole-wp' ), 'disabled' => __( 'Disabled', 'optimole-wp' ), 'enable_avif_title' => __( 'AVIF Image Support', 'optimole-wp' ), 'enable_avif_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Enable this to automatically convert images to the AVIF format on browsers that support it. This format provides quality images with reduced file sizes, and faster webpage loading. %1$sLearn more%2$s', 'optimole-wp' ), '', '' ), 'enable_bg_lazyload_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Apply lazy loading to CSS background images. The toggle below enables it globally. The selector list is optional and only needed to extend coverage if certain elements are missed. %1$sLearn more%2$s', 'optimole-wp' ), '', '' ), 'enable_bg_lazyload_title' => __( 'CSS Background Lazy Load', 'optimole-wp' ), 'enable_video_lazyload_desc' => __( 'Lazy load embedded videos and iframe content', 'optimole-wp' ), 'enable_video_lazyload_title' => __( 'Video & iframes', 'optimole-wp' ), 'enable_noscript_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Enables fallback images for browsers that can\'t handle JavaScript-based lazy loading or related features. Disabling it may resolve conflicts with other plugins or configurations and decrease HTML page size. %1$sLearn more%2$s', 'optimole-wp' ), '', '' ), // translators: %s is the name of the html tag (e.g: