PluginProbe
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization / 4.2.15
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization v4.2.15
4.2.15 4.2.14 4.2.13 4.2.12 4.2.11 4.2.10 4.2.9 4.2.8 4.2.7 4.2.6 4.2.5 2.5.5 2.5.6 2.5.7 3.0.0 3.0.1 3.1.0 3.1.1 3.1.2 3.1.3 3.10.0 3.11.0 3.11.1 3.11.2 3.11.3 All 136 releases
optimole-wp / inc / admin.php

admin.php in Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization 4.2.15, at inc/admin.php

2,564 lines 117.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Admin class.
4 *
5 * Author: Andrei Baicus <[email protected]>
6 * Created on: 19/07/2018
7 *
8 * @soundtrack Somewhere Else - Marillion
9 * @package \Optimole\Inc
10 * @author Optimole <[email protected]>
11 */
12
13 use enshrined\svgSanitize\Sanitizer;
14 use OptimoleWP\BgOptimizer\Lazyload;
15 use OptimoleWP\PageProfiler\Profile;
16 /**
17 * Class Optml_Admin
18 */
19 class Optml_Admin {
20 use Optml_Normalizer;
21
22 const IMAGE_DATA_COLLECTED = 'optml_image_data_collected';
23
24 const IMAGE_DATA_COLLECTED_BATCH = 100;
25
26 const BF_PROMO_DISMISS_KEY = 'optml_bf25_notice_dismiss';
27
28 const SPC_BANNER_DISMISS_KEY = 'optml_spc_banner_dismiss';
29
30 /**
31 * Hold the settings object.
32 *
33 * @var Optml_Settings Settings object.
34 */
35 public $settings;
36
37 /**
38 * Hold the plugin conflict object.
39 *
40 * @var Optml_Conflicting_Plugins Settings object.
41 */
42 public $conflicting_plugins;
43
44 const NEW_USER_DEFAULTS_UPDATED = 'optml_defaults_updated';
45 const OLD_USER_ENABLED_LD = 'optml_enabled_limit_dimensions';
46 const OLD_USER_ENABLED_CL = 'optml_enabled_cloud_sites';
47
48 const SYNC_CRON = 'optml_daily_sync';
49 const ENRICH_CRON = 'optml_pull_image_data';
50 /**
51 * Optml_Admin constructor.
52 */
53 public function __construct() {
54 $this->settings = new Optml_Settings();
55 $this->conflicting_plugins = new Optml_Conflicting_Plugins();
56
57 $media_rename = new Optml_Attachment_Edit();
58 $media_rename->init();
59
60 $dashboard_widget = new Optml_Dashboard_Widget();
61 $dashboard_widget->init();
62
63 add_filter( 'plugin_action_links_' . plugin_basename( OPTML_BASEFILE ), [ $this, 'add_action_links' ] );
64 add_action( 'admin_menu', [ $this, 'add_dashboard_page' ] );
65 add_action( 'admin_menu', [ $this, 'add_settings_subpage' ], 99 );
66 add_action( 'admin_head', [ $this, 'menu_icon_style' ] );
67 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue' ], PHP_INT_MIN );
68 add_action( 'admin_notices', [ $this, 'add_notice' ] );
69 add_action( 'admin_notices', [ $this, 'add_notice_upgrade' ] );
70 add_action( 'admin_notices', [ $this, 'add_notice_conflicts' ] );
71 add_action( self::SYNC_CRON, [ $this, 'daily_sync' ] );
72 add_action( 'admin_init', [ $this, 'redirect_old_dashboard' ] );
73 add_action( 'optml_purge_image_cache', [ $this, 'purge_image_cache' ] );
74 if ( $this->settings->is_connected() ) {
75 add_action( 'init', [ $this, 'check_domain_change' ] );
76 add_action( self::ENRICH_CRON, [ $this, 'pull_image_data' ] );
77
78 // Backwards compatibility for older versions of WordPress < 6.0.0 requiring 3 parameters for this specific filter.
79 $below_6_0_0 = version_compare( get_bloginfo( 'version' ), '6.0.0', '<' );
80 if ( $below_6_0_0 ) {
81 add_filter( 'wp_insert_attachment_data', [ $this, 'legacy_detect_image_title_changes' ], 10, 3 );
82 } else {
83 add_filter( 'wp_insert_attachment_data', [ $this, 'detect_image_title_changes' ], 10, 4 );
84 }
85
86 add_action( 'updated_post_meta', [ $this, 'detect_image_alt_change' ], 10, 4 );
87 add_action( 'added_post_meta', [ $this, 'detect_image_alt_change' ], 10, 4 );
88 add_filter( 'update_attached_file', [ $this, 'listen_update_file' ], 999, 2 );
89 if ( ! wp_next_scheduled( self::ENRICH_CRON ) ) {
90 wp_schedule_event( time() + 10, 'hourly', self::ENRICH_CRON );
91 }
92 }
93 add_action( 'init', [ $this, 'update_default_settings' ] );
94 add_action( 'init', [ $this, 'update_limit_dimensions' ] );
95 add_action( 'init', [ $this, 'update_cloud_sites_default' ] );
96 add_action( 'admin_init', [ $this, 'maybe_redirect' ] );
97 add_action( 'admin_init', [ $this, 'init_no_script' ] );
98 add_action( 'plugins_loaded', [ $this,'add_permission_policy' ] );
99 if ( ! is_admin() && $this->settings->is_connected() && ! wp_next_scheduled( self::SYNC_CRON ) ) {
100 wp_schedule_event( time() + 10, 'twicedaily', self::SYNC_CRON, [] );
101 }
102 add_action( 'optml_after_setup', [ $this, 'register_public_actions' ], 999999 );
103
104 if ( ! function_exists( 'is_wpcom_vip' ) ) {
105 add_filter(
106 'upload_mimes',
107 [
108 $this,
109 'allow_svg',
110 ]
111 ); // phpcs:ignore WordPressVIPMinimum.Hooks.RestrictedHooks.upload_mimes
112 add_filter( 'wp_handle_upload_prefilter', [ $this, 'check_svg_and_sanitize' ] );
113 }
114
115 add_filter( 'themeisle-sdk/survey/' . OPTML_PRODUCT_SLUG, [ $this, 'get_survey_metadata' ], 10, 2 );
116 add_action( 'admin_init', [ $this, 'mark_user_with_offload' ] );
117 }
118
119 /**
120 * Add the permission policy for CH.
121 *
122 * @return void
123 */
124 public function add_permission_policy() {
125 if ( ! $this->settings->is_connected() ) {
126 return;
127 }
128 if ( ! $this->settings->is_enabled() ) {
129 return;
130 }
131 if ( headers_sent() ) {
132 return;
133 }
134 $policy = $this->get_permissions_policy();
135 if ( empty( $policy ) ) {
136 return;
137 }
138 header( sprintf( 'Permissions-Policy: %s', $policy ), false );
139 }
140
141 /**
142 * Build the Permissions-Policy header value based on active settings.
143 *
144 * @return string Comma-separated policy directives, or empty string when none apply.
145 */
146 public function get_permissions_policy(): string {
147 $parts = [];
148 $service_url = esc_url( Optml_Config::$service_url );
149
150 if ( $this->settings->is_scale_enabled() ) {
151 $parts[] = sprintf( 'ch-viewport-width=(self "%s")', $service_url );
152 }
153 if ( $this->settings->get( 'network_optimization' ) === 'enabled' ) {
154 $parts[] = sprintf( 'ch-ect=(self "%s")', $service_url );
155 }
156 if ( $this->settings->get( 'retina_images' ) === 'enabled' ) {
157 $parts[] = sprintf( 'ch-dpr=(self "%s")', $service_url );
158 }
159
160 return implode( ', ', $parts );
161 }
162 /**
163 * Function that purges the image cache for a specific file.
164 *
165 * @param string $file Path or url of the file to clear.
166 *
167 * @return void
168 */
169 public function purge_image_cache( $file ) {
170 $basename = wp_basename( $file );
171 $settings = new Optml_Settings();
172 $settings->clear_cache( $basename );
173 }
174 /**
175 * Listen when the file is updated and clear the cache for the file.
176 *
177 * @param string $file The file path.
178 * @param int $post_id The post ID.
179 *
180 * @return string The file path.
181 */
182 public function listen_update_file( $file, $post_id ) {
183 // Purge the image cache.
184 do_action( 'optml_purge_image_cache', $file );
185 return $file;
186 }
187 /**
188 * Check if the file is an SVG, if so handle appropriately
189 *
190 * @param array $file An array of data for a single file.
191 *
192 * @return mixed
193 */
194 public function check_svg_and_sanitize( $file ) {
195 // Ensure we have a proper file path before processing.
196 if ( ! isset( $file['tmp_name'] ) ) {
197 return $file;
198 }
199
200 $file_name = isset( $file['name'] ) ? $file['name'] : '';
201 $wp_filetype = wp_check_filetype_and_ext( $file['tmp_name'], $file_name );
202 $type = ! empty( $wp_filetype['type'] ) ? $wp_filetype['type'] : '';
203
204 if ( 'image/svg+xml' === $type ) {
205 if ( ! current_user_can( 'upload_files' ) ) {
206 $file['error'] = 'Invalid';
207 return $file;
208 }
209
210 if ( ! $this->sanitize_svg( $file['tmp_name'] ) ) {
211 $file['error'] = 'Invalid';
212 }
213 }
214
215 return $file;
216 }
217
218 /**
219 * Sanitize the SVG
220 *
221 * @param string $file Temp file path.
222 *
223 * @return bool|int
224 */
225 public function sanitize_svg( $file ) {
226 // We can ignore the phpcs warning here as we're reading and writing to the Temp file.
227 $dirty = file_get_contents( $file ); // phpcs:ignore
228
229 // Is the SVG gzipped? If so we try and decode the string.
230 $is_zipped = $this->is_gzipped( $dirty );
231 if ( $is_zipped && ( ! function_exists( 'gzdecode' ) || ! function_exists( 'gzencode' ) ) ) {
232 return false;
233 }
234
235 if ( $is_zipped ) {
236 $dirty = gzdecode( $dirty );
237
238 // If decoding fails, bail as we're not secure.
239 if ( false === $dirty ) {
240 return false;
241 }
242 }
243
244 $sanitizer = new Sanitizer();
245 $clean = $sanitizer->sanitize( $dirty );
246
247 if ( false === $clean ) {
248 return false;
249 }
250
251 // If we were gzipped, we need to re-zip.
252 if ( $is_zipped ) {
253 $clean = gzencode( $clean );
254 }
255
256 // We handle the write result below; silence the warning on I/O failure. Reading/writing the temp file is intended.
257 $written = @file_put_contents( $file, $clean ); // phpcs:ignore WordPress.WP.AlternativeFunctions, WordPress.PHP.NoSilencedErrors
258
259 // A failed write leaves the dirty upload in place, so report it as unsanitized.
260 return is_string( $clean ) && strlen( $clean ) === $written;
261 }
262
263 /**
264 * Check if the contents are gzipped
265 *
266 * @see http://www.gzip.org/zlib/rfc-gzip.html#member-format
267 *
268 * @param string $contents Content to check.
269 *
270 * @return bool
271 */
272 protected function is_gzipped( $contents ) {
273 // phpcs:disable Generic.Strings.UnnecessaryStringConcat.Found
274 if ( function_exists( 'mb_strpos' ) ) {
275 return 0 === mb_strpos( $contents, "\x1f" . "\x8b" . "\x08" );
276 } else {
277 return 0 === strpos( $contents, "\x1f" . "\x8b" . "\x08" );
278 }
279 // phpcs:enable
280 }
281
282 /**
283 * Query the database for images and extract the alt/title to send them to the API
284 *
285 * @uses action: optml_pull_image_data
286 */
287 public function pull_image_data() {
288 // Get all image attachments that are not processed
289 $args = [
290 'post_type' => 'attachment',
291 'post_mime_type' => 'image',
292 'posts_per_page' => self::IMAGE_DATA_COLLECTED_BATCH,
293 'meta_query' => [
294 'relation' => 'AND',
295 [
296 'key' => self::IMAGE_DATA_COLLECTED,
297 'compare' => 'NOT EXISTS',
298 ],
299 ],
300 ];
301 $attachments = get_posts( $args );
302
303 $image_data = [];
304
305 foreach ( $attachments as $attachment ) {
306 // Get image URL, alt, and title
307 $image_url = wp_get_attachment_url( $attachment->ID );
308 $image_alt = get_post_meta( $attachment->ID, '_wp_attachment_image_alt', true );
309 $image_title = $attachment->post_title;
310 $image_data[ $image_url ] = [];
311
312 if ( ! empty( $image_alt ) ) {
313 $image_data[ $image_url ]['alt'] = $image_alt;
314 }
315 if ( ! empty( $image_title ) ) {
316 $image_data[ $image_url ]['title'] = $image_title;
317 }
318 if ( empty( $image_data[ $image_url ] ) ) {
319 unset( $image_data[ $image_url ] );
320 }
321
322 // Mark the image as processed
323 update_post_meta( $attachment->ID, self::IMAGE_DATA_COLLECTED, 'yes' );
324 }
325
326 if ( ! empty( $image_data ) ) {
327 $api = new Optml_Api();
328 $api->call_data_enrich_api( $image_data );
329 }
330 }
331
332 /**
333 * Delete the processed meta from an image when the alt text is changed
334 *
335 * @uses action: updated_post_meta, added_post_meta
336 */
337 public function detect_image_alt_change( $meta_id, $post_id, $meta_key, $meta_value ) {
338
339 // Check if the updated metadata is for alt text and it's an attachment
340 if ( $meta_key === '_wp_attachment_image_alt' && get_post_type( $post_id ) === 'attachment' ) {
341 delete_post_meta( $post_id, self::IMAGE_DATA_COLLECTED );
342 }
343 }
344
345 /**
346 * Delete the processed meta from an image when the title is changed
347 *
348 * @uses filter: wp_insert_attachment_data
349 */
350 public function detect_image_title_changes( $data, $postarr, $unsanitized_postarr, $update ) {
351
352 // Check if it's an attachment being updated
353 if ( $data['post_type'] !== 'attachment' ) {
354 return $data;
355 }
356 if ( ! isset( $postarr['ID'] ) ) {
357 return $data;
358 }
359 if ( isset( $postarr['post_title'] ) && isset( $postarr['original_post_title'] ) && $postarr['post_title'] !== $postarr['original_post_title'] ) {
360 delete_post_meta( $postarr['ID'], self::IMAGE_DATA_COLLECTED );
361 }
362
363 return $data;
364 }
365
366 /**
367 * Delete the processed meta from an image when the title is changed
368 *
369 * @uses filter: wp_insert_attachment_data
370 */
371 public function legacy_detect_image_title_changes( $data, $postarr, $unsanitized_postarr ) {
372 return $this->detect_image_title_changes( $data, $postarr, $unsanitized_postarr, true );
373 }
374
375 /**
376 * Init no_script setup value based on whether the user is connected or not.
377 */
378 public function init_no_script() {
379 if ( $this->settings->is_connected() ) {
380 $raw_settings = $this->settings->get_raw_settings();
381 if ( ! isset( $raw_settings['no_script'] ) ) {
382 $this->settings->update( 'no_script', 'enabled' );
383 }
384 }
385 }
386
387 /**
388 * Checks if domain has changed
389 */
390 public function check_domain_change() {
391 $previous_domain = get_option( 'optml_current_domain', 0 );
392 $site_url = $this->to_domain_hash( get_home_url() );
393
394 if ( $site_url !== $previous_domain ) {
395 update_option( 'optml_current_domain', $site_url );
396 if ( $previous_domain !== 0 ) {
397 $this->daily_sync();
398 }
399 }
400 }
401
402 /**
403 * Update the limit dimensions setting to enabled if the user is new.
404 */
405 public function update_default_settings() {
406 if ( get_option( self::NEW_USER_DEFAULTS_UPDATED ) === 'yes' ) {
407 return;
408 }
409
410 if ( $this->settings->is_connected() ) {
411 update_option( self::NEW_USER_DEFAULTS_UPDATED, 'yes' );
412
413 return;
414 }
415
416 $this->settings->update( 'limit_dimensions', 'enabled' );
417 $this->settings->update( 'lazyload', 'enabled' );
418 $this->settings->update( 'skip_lazyload_images', '2' );
419 $this->settings->update( 'compression_mode', 'speed_optimized' );
420
421 update_option( self::NEW_USER_DEFAULTS_UPDATED, 'yes' );
422 }
423
424 /**
425 * Enable limit dimensions for old users after removing the Resize option.
426 *
427 * @return void
428 */
429 public function update_limit_dimensions() {
430 if ( get_option( self::OLD_USER_ENABLED_LD ) === 'yes' ) {
431 return;
432 }
433
434 // New users already have this enabled as we changed defaults.
435 if ( ! $this->settings->is_connected() ) {
436 return;
437 }
438
439 $this->settings->update( 'limit_dimensions', 'enabled' );
440
441 update_option( self::OLD_USER_ENABLED_LD, 'yes' );
442 }
443
444 /**
445 * Enable cloud sites and add the current site to the whitelist.
446 *
447 * @return void
448 */
449 public function update_cloud_sites_default() {
450 if ( get_option( self::OLD_USER_ENABLED_CL ) === 'yes' ) {
451 return;
452 }
453
454 if ( ! $this->settings->is_connected() ) {
455 return;
456 }
457
458 // This is already enabled. Don't alter it.
459 if ( $this->settings->get( 'cloud_images' ) === 'enabled' ) {
460 update_option( self::OLD_USER_ENABLED_CL, 'yes' );
461
462 return;
463 }
464
465 $this->settings->update( 'cloud_images', 'enabled' );
466 $this->settings->update( 'cloud_sites', $this->settings->get_cloud_sites_whitelist_default() );
467
468 update_option( self::OLD_USER_ENABLED_CL, 'yes' );
469 }
470
471 /**
472 * Register public actions.
473 */
474 public function register_public_actions() {
475 add_action( 'wp_head', [ $this, 'generator' ] );
476 add_filter( 'wp_resource_hints', [ $this, 'add_dns_prefetch' ], 10, 2 );
477
478 if ( Optml_Manager::should_ignore_image_tags() ) {
479 return;
480 }
481
482 if ( ! $this->settings->use_lazyload()
483 || ( $this->settings->get( 'native_lazyload' ) === 'enabled'
484 && $this->settings->get( 'video_lazyload' ) === 'disabled'
485 && $this->settings->get( 'bg_replacer' ) === 'disabled' ) ) {
486 return;
487 }
488 add_action( 'wp_enqueue_scripts', [ $this, 'frontend_scripts' ] );
489 add_action( 'wp_head', [ $this, 'inline_bootstrap_script' ] );
490
491 add_filter( 'optml_additional_html_classes', [ $this, 'add_no_js_class_to_html_tag' ], 10 );
492 }
493
494 /**
495 * Use filter to add additional class to html tag.
496 *
497 * @param array $classes The classes to be added.
498 *
499 * @return array
500 */
501 public function add_no_js_class_to_html_tag( $classes ) {
502 // we need the space padding since some plugins might not target correctly with js there own classes
503 // this causes some issues if they concat directly, this way we can protect against that since no matter what
504 // there will be an extra space padding so that classes can be easily identified
505 return array_merge( $classes, [ ' optml_no_js ' ] );
506 }
507
508 /**
509 * Adds script for lazyload/js replacement.
510 */
511 public function inline_bootstrap_script() {
512 $domain = 'https://' . $this->settings->get_cdn_url() . '/js-lib';
513
514 if ( defined( 'OPTML_JS_CDN' ) && constant( 'OPTML_JS_CDN' ) ) {
515 $domain = 'https://' . constant( 'OPTML_JS_CDN' ) . '/js-lib';
516 }
517
518 $min = ! OPTML_DEBUG ? '.min' : '';
519 $bgclasses = Optml_Lazyload_Replacer::get_lazyload_bg_classes();
520 $watcher_classes = Optml_Lazyload_Replacer::get_watcher_lz_classes();
521 $lazyload_bg_selectors = Optml_Lazyload_Replacer::get_background_lazyload_selectors();
522 foreach ( $bgclasses as $key ) {
523 $lazyload_bg_selectors[] = '.' . $key;
524 }
525 $lazyload_bg_selectors = empty( $lazyload_bg_selectors ) ? '' : sprintf( '%s', implode( ', ', (array) $lazyload_bg_selectors ) );
526 $bgclasses = empty( $bgclasses ) ? '' : sprintf( '"%s"', implode( '","', (array) $bgclasses ) );
527 $watcher_classes = empty( $watcher_classes ) ? '' : sprintf( '"%s"', implode( '","', (array) $watcher_classes ) );
528 $default_network = ( $this->settings->get( 'network_optimization' ) === 'enabled' );
529 $limit_dimensions = $this->settings->get( 'limit_dimensions' ) === 'enabled';
530 $limit_width = $limit_dimensions ? $this->settings->get( 'limit_width' ) : 0;
531 $limit_height = $limit_dimensions ? $this->settings->get( 'limit_height' ) : 0;
532 $retina_ready = ( $this->settings->get( 'retina_images' ) !== 'enabled' );
533 $scale_is_disabled = ( $this->settings->get( 'scale' ) === 'enabled' );
534 $native_lazy_enabled = ( $this->settings->get( 'native_lazyload' ) === 'enabled' );
535 $output = sprintf(
536 '
537 <style type="text/css">
538 img[data-opt-src]:not([data-opt-lazy-loaded]) {
539 transition: .2s filter linear, .2s opacity linear, .2s border-radius linear;
540 -webkit-transition: .2s filter linear, .2s opacity linear, .2s border-radius linear;
541 -moz-transition: .2s filter linear, .2s opacity linear, .2s border-radius linear;
542 -o-transition: .2s filter linear, .2s opacity linear, .2s border-radius linear;
543 }
544 img[data-opt-src]:not([data-opt-lazy-loaded]) {
545 opacity: .75;
546 -webkit-filter: blur(8px);
547 -moz-filter: blur(8px);
548 -o-filter: blur(8px);
549 -ms-filter: blur(8px);
550 filter: blur(8px);
551 transform: scale(1.04);
552 animation: 0.1s ease-in;
553 -webkit-transform: translate3d(0, 0, 0);
554 }
555 %s
556 </style>
557 <script type="application/javascript">
558 document.documentElement.className = document.documentElement.className.replace(/\boptml_no_js\b/g, "");
559 (function(w, d){
560 var b = d.getElementsByTagName("head")[0];
561 var s = d.createElement("script");
562 var v = ("IntersectionObserver" in w && "isIntersecting" in w.IntersectionObserverEntry.prototype) ? "_no_poly" : "";
563 s.async = true;
564 s.src = "%s/v2/latest/optimole_lib" + v + "%s.js";
565 b.appendChild(s);
566 w.optimoleData = {
567 lazyloadOnly: "optimole-lazy-only",
568 backgroundReplaceClasses: [%s],
569 nativeLazyload : %s,
570 scalingDisabled: %s,
571 watchClasses: [%s],
572 backgroundLazySelectors: "%s",
573 network_optimizations: %s,
574 ignoreDpr: %s,
575 quality: %d,
576 maxWidth: %d,
577 maxHeight: %d,
578 }
579 }(window, document));
580 </script>',
581 Optml_Lazyload_Replacer::IFRAME_TEMP_COMMENT,
582 esc_url( $domain ),
583 $min,
584 wp_strip_all_tags( $bgclasses ),
585 $native_lazy_enabled ? 'true' : 'false',
586 $scale_is_disabled ? 'true' : 'false',
587 wp_strip_all_tags( $watcher_classes ),
588 addcslashes( wp_strip_all_tags( $lazyload_bg_selectors ), '"' ),
589 defined( 'OPTML_NETWORK_ON' ) && constant( 'OPTML_NETWORK_ON' ) ? ( OPTML_NETWORK_ON ? 'true' : 'false' ) : ( $default_network ? 'true' : 'false' ),
590 $retina_ready ? 'true' : 'false',
591 $this->settings->get_numeric_quality(),
592 $limit_width,
593 $limit_height
594 );
595 echo $output;
596 }
597
598 /**
599 * Add settings links in the plugin listing page.
600 *
601 * @param string[]|mixed $links Old plugin links.
602 *
603 * @return string[]|mixed Altered links.
604 */
605 public function add_action_links( $links ) {
606 if ( ! is_array( $links ) ) {
607 return $links;
608 }
609 return array_merge(
610 $links,
611 [
612 '<a href="' . admin_url( 'admin.php?page=optimole' ) . '">' . __( 'Settings', 'optimole-wp' ) . '</a>',
613 ]
614 );
615 }
616
617 /**
618 * Check if we should show the notice.
619 *
620 * @return bool Should show?
621 */
622 public function should_show_notice() {
623 if ( wp_doing_ajax() ) {
624 return false;
625 }
626
627 if ( is_network_admin() ) {
628 return false;
629 }
630
631 if ( ! current_user_can( 'manage_options' ) ) {
632 return false;
633 }
634
635 if ( $this->settings->is_connected() ) {
636 return false;
637 }
638
639 $current_screen = get_current_screen();
640
641 if ( empty( $current_screen ) ) {
642 return false;
643 }
644
645 if ( ( get_option( 'optml_notice_optin', 'no' ) === 'yes' ) ) {
646 return false;
647 }
648
649 return true;
650 }
651
652 /**
653 * Show upgrade notice.
654 */
655 public function add_notice_upgrade() {
656 if ( ! $this->should_show_upgrade() ) {
657 return;
658 }
659 ?>
660 <div class="notice optml-notice-optin"
661 style="background-color: #577BF9; color:white; border: none !important; display: flex; align-items: center;">
662 <div style="margin: 1% 2%;">
663 <img style="max-width: 100px;" src='<?php echo OPTML_URL . 'assets/img/upgrade_icon.png'; ?>'>
664 </div>
665 <div style="display: grid; gap: 10px;">
666 <p style="font-size: 16px !important;">
667 <?php
668 printf(
669 /* translators: 1 - visits limit */
670 __( 'You\'re nearing your %1$s-visit monthly cap on Optimole. If you exceed it, we\'ll serve original (unoptimized) images - expect slower pages.', 'optimole-wp' ),
671 '<strong>' . number_format_i18n( 2000 ) . '</strong>'
672 );
673 ?>
674 </p>
675 <p style="margin: 1.5% 0;">
676 <a href="<?php echo esc_url( tsdk_translate_link( self::get_upgrade_base_link() ) ); ?>"
677 target="_blank"
678 style="border-radius: 4px;padding: 9px 10px;border: 2px solid #FFF;color: white;text-decoration: none;"><?php _e( 'Check upgrade plans', 'optimole-wp' ); ?>
679 </a>
680 <a style="padding: 2%; color: white;"
681 href="<?php echo wp_nonce_url( add_query_arg( [ 'optml_hide_upg' => 'yes' ] ), 'hide_nonce', 'optml_nonce' ); ?>"><?php _e( 'I have already done this', 'optimole-wp' ); ?></a>
682 </p>
683 </div>
684 </div>
685 <?php
686 }
687
688 /**
689 * Check if we should show the upgrade notice to users.
690 *
691 * @return bool Should we show it?
692 */
693 public function should_show_upgrade() {
694 $current_screen = get_current_screen();
695 if ( wp_doing_ajax() ||
696 is_network_admin() ||
697 ! current_user_can( 'manage_options' ) ||
698 ! $this->settings->is_connected() ||
699 empty( $current_screen )
700 ) {
701 return false;
702 }
703 if ( get_option( 'optml_notice_hide_upg', 'no' ) === 'yes' ) {
704 return false;
705 }
706 if ( $current_screen->base !== 'upload' ) {
707 return false;
708 }
709 $service_data = $this->settings->get( 'service_data' );
710 if ( ! isset( $service_data['plan'] ) ) {
711 return false;
712 }
713 if ( $service_data['plan'] !== 'free' ) {
714 return false;
715 }
716 $visitors_limit = isset( $service_data['visitors_limit'] ) ? (int) $service_data['visitors_limit'] : 0;
717 $visitors_left = isset( $service_data['visitors_left'] ) ? (int) $service_data['visitors_left'] : 0;
718 if ( $visitors_limit === 0 ) {
719 return false;
720 }
721 if ( $visitors_left > 300 ) {
722 return false;
723 }
724
725 return true;
726 }
727
728 /**
729 * Build upgrade URL with current user's encoded email.
730 *
731 * @return string Upgrade URL.
732 */
733 public static function get_upgrade_base_link() {
734 $base_url = 'https://optimole.com/upgrade';
735 $base_url = tsdk_translate_link( $base_url );
736 $email = self::get_account_email();
737
738 if ( ! empty( $email ) ) {
739 $base_url = add_query_arg( 'email', base64_encode( $email ), $base_url );
740 }
741
742 return $base_url;
743 }
744
745 /**
746 * Get contact URL.
747 *
748 * @return string Contact URL.
749 */
750 public static function get_contact_base_link() {
751 return tsdk_translate_link( 'https://optimole.com/contact' );
752 }
753
754 /**
755 * Retrieve Optimole account email.
756 *
757 * @return string
758 */
759 private static function get_account_email() {
760 $settings = new Optml_Settings();
761 $service_data = $settings->get( 'service_data' );
762
763 if ( isset( $service_data['user_email'] ) && is_string( $service_data['user_email'] ) ) {
764 $email = sanitize_email( $service_data['user_email'] );
765 if ( ! empty( $email ) ) {
766 return $email;
767 }
768 }
769
770 return '';
771 }
772
773 /**
774 * CSS styles for Notice.
775 */
776 public static function notice_styles() {
777 ?>
778 <style>
779 .optml-notice-optin:not(.has-dismiss) {
780 background: url(" <?php echo esc_attr( OPTML_URL . '/assets/img/disconnected.svg' ); ?> ") #fff 100% 0 no-repeat;
781 position: relative;
782 padding: 0;
783 }
784
785 .optml-notice-optin.has-dismiss {
786 position: relative;
787 }
788
789 .optml-notice-optin .content {
790 background: rgba(255, 255, 255, 0.75);
791 display: flex;
792 align-items: center;
793 padding: 20px;
794 }
795
796 .optml-notice-optin img {
797 max-width: 100px;
798 margin-right: 20px;
799 display: none;
800 }
801
802 .optml-notice-optin .description {
803 font-size: 14px;
804 margin-bottom: 20px;
805 color: #000;
806 }
807
808 .optml-notice-optin .actions {
809 margin-top: auto;
810 display: flex;
811 gap: 20px;
812 }
813
814 @media screen and (min-width: 768px) {
815 .optml-notice-optin img {
816 display: block;
817 }
818 }
819 </style>
820 <?php
821 }
822
823 /**
824 * JS for Notice.
825 */
826 public static function notice_js( $action ) {
827 ?>
828 <script>
829 jQuery(document).ready(function ($) {
830 // AJAX request to update the option value
831 $('.optml-notice-optin button.notice-dismiss').click(function (e) {
832 e.preventDefault();
833
834 var notice = $(this).closest('.optml-notice-optin');
835 var nonce = '<?php echo esc_attr( wp_create_nonce( $action ) ); ?>';
836
837 $.ajax({
838 url: window.ajaxurl,
839 type: 'POST',
840 data: {
841 action: '<?php echo esc_attr( $action ); ?>',
842 nonce
843 },
844 complete() {
845 notice.remove();
846 }
847 });
848 });
849 });
850 </script>
851 <?php
852 }
853
854 /**
855 * Adds opt in notice.
856 */
857 public function add_notice() {
858 if ( ! $this->should_show_notice() ) {
859 return;
860 }
861
862 self::notice_styles();
863 ?>
864 <div class="notice notice-info optml-notice-optin">
865 <div class="content">
866 <img src="<?php echo OPTML_URL . '/assets/img/logo.svg'; ?>"
867 alt="<?php echo esc_attr__( 'Logo', 'optimole-wp' ); ?>"/>
868
869 <div>
870 <p class="notice-title"> <?php echo esc_html__( 'Finish setting up!', 'optimole-wp' ); ?></p>
871 <p class="description">
872 <?php
873 printf(
874 /* translators: 1 - opening strong tag, 2 - closing strong tag, 3 - opening strong tag, 4 - closing strong tag */
875 __( 'Welcome to %1$sOptimole%2$s, the easiest way to optimize your website images. Your users will enjoy a %3$sfaster%4$s website after you connect it with our service.', 'optimole-wp' ),
876 '<strong>',
877 '</strong>',
878 '<strong>',
879 '</strong>'
880 );
881 ?>
882 </p>
883 <div class="actions">
884 <a href="<?php echo esc_url( admin_url( 'admin.php?page=optimole' ) ); ?>"
885 class="button button-primary button-hero"><?php _e( 'Connect to Optimole', 'optimole-wp' ); ?>
886 </a>
887 <a class="button button-secondary button-hero"
888 href="<?php echo wp_nonce_url( add_query_arg( [ 'optml_hide_optin' => 'yes' ] ), 'hide_nonce', 'optml_nonce' ); ?>"><?php _e( 'I will do it later', 'optimole-wp' ); ?>
889 </a>
890 </div>
891 </div>
892 </div>
893 </div>
894 <?php
895 }
896
897 /**
898 * Adds conflicts notice.
899 */
900 public function add_notice_conflicts() {
901 if ( ! $this->conflicting_plugins->should_show_notice() ) {
902 return;
903 }
904
905 $plugins = $this->conflicting_plugins->get_conflicting_plugins();
906 $names = [];
907
908 foreach ( $plugins as $plugin ) {
909 $plugin_data = get_plugin_data( WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . $plugin );
910 $names[] = $plugin_data['Name'];
911 }
912
913 $names = implode( ', ', $names );
914
915 self::notice_styles();
916 self::notice_js( 'optml_dismiss_conflict_notice' );
917 ?>
918 <div class="notice notice-info optml-notice-optin has-dismiss">
919 <div class="content">
920 <img src="<?php echo OPTML_URL . '/assets/img/logo.svg'; ?>"
921 alt="<?php echo esc_attr__( 'Logo', 'optimole-wp' ); ?>"/>
922
923 <div>
924 <p class="notice-title">
925 <strong><?php echo esc_html__( 'Oops... Multiple image optimization plugins active', 'optimole-wp' ); ?></strong>
926 </p>
927 <p class="description">
928 <?php
929 printf(
930 /* translators: 1 - plugin name, 2 - opening bold tag, 3 - closing bold tag */
931 __( 'We noticed multiple image optimization plugins active on your site, which may cause issues in Optimole. We recommend using only one image optimization plugin on your site for the best results. The following plugins may cause issues in Optimole: %2$s%1$s%3$s.', 'optimole-wp' ),
932 $names,
933 '<strong>',
934 '</strong>'
935 );
936 ?>
937 </p>
938 <div class="actions">
939 <a href="<?php echo esc_url( admin_url( 'plugins.php?optimole_conflicts' ) ); ?>"
940 class="button button-primary button-hero"><?php _e( 'Manage Plugins', 'optimole-wp' ); ?>
941 </a>
942 </div>
943 </div>
944 </div>
945
946 <button type="button" class="notice-dismiss">
947 <span class="screen-reader-text"><?php _e( 'Dismiss this notice.', 'optimole-wp' ); ?></span>
948 </button>
949 </div>
950 <?php
951 }
952
953 /**
954 * Add style classes for lazy loading background images.
955 */
956 protected function get_background_lazy_css() {
957
958 $watchers = Optml_Lazyload_Replacer::get_background_lazyload_selectors();
959
960 $css = [];
961 foreach ( $watchers as $selector ) {
962 $css[] = 'html ' . $selector . ':not(.optml-bg-lazyloaded)';
963 }
964 if ( empty( $css ) ) {
965 return '';
966 }
967 $css = implode( ",\n", $css ) . ' { background-image: none !important; }';
968
969 $css = Lazyload::MARKER . "\n" . strip_tags( $css ) . "\n" . Lazyload::MARKER;
970 return $css;
971 }
972
973 /**
974 * Enqueue frontend scripts.
975 */
976 public function frontend_scripts() {
977
978 $bg_css = $this->get_background_lazy_css();
979
980 wp_register_style( 'optm_lazyload_noscript_style', false );
981 wp_enqueue_style( 'optm_lazyload_noscript_style' );
982 wp_add_inline_style( 'optm_lazyload_noscript_style', "html.optml_no_js img[data-opt-src] { display: none !important; } \n " . $bg_css );
983
984 if ( $this->settings->use_lazyload() === true ) {
985 wp_register_script( 'optml-print', false );
986 wp_enqueue_script( 'optml-print' );
987 $script = '
988 (function(w, d){
989 w.addEventListener("beforeprint", function(){
990 let images = d.getElementsByTagName( "img" );
991 for (let img of images) {
992 if ( !img.dataset.optSrc) {
993 continue;
994 }
995 img.src = img.dataset.optSrc;
996 delete img.dataset.optSrc;
997 }
998 });
999
1000 }(window, document));
1001 ';
1002 wp_add_inline_script( 'optml-print', $script );
1003 }
1004 if ( Optml_Manager::should_load_profiler() ) {
1005 add_action(
1006 'wp_footer',
1007 function () {
1008
1009 print ( self::get_optimizer_script() );
1010 }
1011 );
1012 }
1013 print ( self::get_preload_links_placeholder() );
1014 }
1015 /**
1016 * Get the optimizer script.
1017 *
1018 * @param bool $placeholder Whether to return the placeholder or the actual script.
1019 *
1020 * @return string The optimizer script.
1021 */
1022 public static function get_optimizer_script( $placeholder = true ) {
1023
1024 if ( $placeholder ) {
1025 return '<script id=optmloptimizer></script>';
1026 }
1027
1028 return '<script async src="' . add_query_arg( 'v', OPTML_VERSION, OPTML_URL . 'assets/build/optimizer/optimizer.js' ) . '"></script><script id="optmloptimizer" >
1029 var optimoleDataOptimizer = ' . wp_json_encode(
1030 [
1031 'restUrl' => esc_js( untrailingslashit( rest_url( OPTML_NAMESPACE . '/v1' ) ) ),
1032 'nonce' => esc_js( wp_create_nonce( 'wp_rest' ) ),
1033 'missingDevices' => esc_js( Profile::PLACEHOLDER_MISSING ),
1034 'pageProfileId' => esc_js( Profile::PLACEHOLDER ),
1035 'pageProfileUrl' => esc_js( Profile::PLACEHOLDER_URL ),
1036 '_t' => esc_js( Profile::PLACEHOLDER_TIME ),
1037 'hmac' => esc_js( Profile::PLACEHOLDER_HMAC ),
1038 'bgSelectors' => array_values( Optml_Lazyload_Replacer::get_background_lazyload_selectors() ),
1039 ]
1040 ) . ';
1041 </script>';
1042 }
1043
1044 /**
1045 * Get the preload links placeholder.
1046 *
1047 * @return string The preload links placeholder.
1048 */
1049 public static function get_preload_links_placeholder(): string {
1050 return '<script id=optmlpreload></script>';
1051 }
1052 /**
1053 * Maybe redirect to dashboard page.
1054 */
1055 public function maybe_redirect() {
1056 if ( isset( $_GET['optml_nonce'] ) && isset( $_GET['optml_hide_optin'] ) && $_GET['optml_hide_optin'] === 'yes' && wp_verify_nonce( $_GET['optml_nonce'], 'hide_nonce' ) ) {
1057 update_option( 'optml_notice_optin', 'yes' );
1058 }
1059
1060 if ( isset( $_GET['optml_nonce'] ) && isset( $_GET['optml_hide_upg'] ) && $_GET['optml_hide_upg'] === 'yes' && wp_verify_nonce( $_GET['optml_nonce'], 'hide_nonce' ) ) {
1061 update_option( 'optml_notice_hide_upg', 'yes' );
1062 }
1063
1064 if ( ! get_transient( 'optml_fresh_install' ) ) {
1065 return;
1066 }
1067
1068 if ( wp_doing_ajax() ) {
1069 return;
1070 }
1071
1072 delete_transient( 'optml_fresh_install' );
1073
1074 if ( is_network_admin() || isset( $_GET['activate-multi'] ) ) {
1075 return;
1076 }
1077
1078 if ( $this->settings->is_connected() ) {
1079 return;
1080 }
1081
1082 wp_safe_redirect( admin_url( 'admin.php?page=optimole' ) );
1083 exit;
1084 }
1085
1086 /**
1087 * Output Generator tag.
1088 */
1089 public function generator() {
1090 if ( ! $this->settings->is_connected() ) {
1091 return;
1092 }
1093 if ( ! $this->settings->is_enabled() ) {
1094 return;
1095 }
1096
1097 $hints = $this->get_accept_ch_hints();
1098 if ( empty( $hints ) ) {
1099 return;
1100 }
1101 echo sprintf( '<meta http-equiv="Accept-CH" content="%s" />', esc_attr( $hints ) );
1102 }
1103
1104 /**
1105 * Build the Accept-CH meta content value based on active settings.
1106 *
1107 * Mirrors the directives used in get_permissions_policy() so both
1108 * the Permissions-Policy header and the Accept-CH meta stay in sync.
1109 *
1110 * @return string Comma-separated hint tokens, or empty string when none apply.
1111 */
1112 public function get_accept_ch_hints(): string {
1113 $hints = [];
1114
1115 if ( $this->settings->is_scale_enabled() ) {
1116 $hints[] = 'Viewport-Width';
1117 }
1118 if ( $this->settings->get( 'network_optimization' ) === 'enabled' ) {
1119 $hints[] = 'ECT';
1120 }
1121 if ( $this->settings->get( 'retina_images' ) === 'enabled' ) {
1122 $hints[] = 'DPR';
1123 }
1124
1125 return implode( ', ', $hints );
1126 }
1127
1128 /**
1129 * Update daily the quota routine.
1130 */
1131 public function daily_sync() {
1132
1133 $api_key = $this->settings->get( 'api_key' );
1134 $service_data = $this->settings->get( 'service_data' );
1135 $application = '';
1136
1137 if ( isset( $service_data['cdn_key'] ) ) {
1138 $application = $service_data['cdn_key'];
1139 }
1140
1141 if ( empty( $api_key ) ) {
1142 return;
1143 }
1144
1145 $request = new Optml_Api();
1146 $data = $request->get_user_data( $api_key, $application );
1147 if ( $data === false || is_wp_error( $data ) ) {
1148 return;
1149 }
1150 if ( $data === 'disconnect' ) {
1151 $settings = new Optml_Settings();
1152 $settings->reset();
1153
1154 return;
1155 }
1156
1157 add_filter( 'optml_dont_trigger_settings_updated', '__return_true' );
1158 $this->settings->update( 'service_data', $data );
1159
1160 if ( isset( $data['extra_visits'] ) ) {
1161 $this->settings->update_frontend_banner_from_remote( $data['extra_visits'] );
1162 }
1163
1164 // Here the account got deactivated, in this case we check if the user is using offloaded images and we roll them back.
1165 $should_revert_offloading = isset( $data['status'] ) && $data['status'] === 'inactive';
1166
1167 // The user is now on a plan without offloading and the grace period is over.
1168 if ( isset( $data['should_revert_offload'] ) && (bool) $data['should_revert_offload'] ) {
1169 $should_revert_offloading = true;
1170 }
1171
1172 if ( $should_revert_offloading ) {
1173 // We check if the user has images offloaded.
1174 if ( $this->settings->get( 'offload_media' ) === 'disabled' ) {
1175 return;
1176 }
1177
1178 $in_progress = $this->settings->get( 'offloading_status' ) !== 'disabled';
1179 // We check if there is an in progress transfer, we stop it.
1180 if ( $in_progress ) {
1181 $this->settings->update( 'offloading_status', 'disabled' );
1182 }
1183 $this->settings->update( 'rollback_status', 'enabled' );
1184 // We start the rollback process.
1185 Optml_Logger::instance()->add_log( 'rollback_images', 'Account deactivated, starting rollback.' );
1186 Optml_Media_Offload::move_images( 'rollback_images', false );
1187 $this->settings->update( 'offload_media', 'disabled' );
1188 }
1189 remove_filter( 'optml_dont_trigger_settings_updated', '__return_true' );
1190 }
1191
1192 /**
1193 * Adds cdn url for prefetch.
1194 *
1195 * @param array $hints Hints array.
1196 * @param string $relation_type Type of relation.
1197 *
1198 * @return array Altered hints array.
1199 */
1200 public function add_dns_prefetch( $hints, $relation_type ) {
1201 if ( 'dns-prefetch' !== $relation_type &&
1202 'preconnect' !== $relation_type
1203 ) {
1204 return $hints;
1205 }
1206 if ( ! $this->settings->is_connected() ) {
1207 return $hints;
1208 }
1209 if ( ! $this->settings->is_enabled() ) {
1210 return $hints;
1211 }
1212 $hints[] = sprintf( 'https://%s', $this->settings->get_cdn_url() );
1213
1214 return $hints;
1215 }
1216
1217 /**
1218 * Add the dashboard page.
1219 */
1220 public function add_dashboard_page() {
1221 if ( defined( 'OPTIOMLE_HIDE_ADMIN_AREA' ) && OPTIOMLE_HIDE_ADMIN_AREA ) {
1222 return;
1223 }
1224
1225 add_menu_page(
1226 'Optimole',
1227 'Optimole',
1228 'manage_options',
1229 'optimole',
1230 [ $this, 'render_dashboard_page' ],
1231 OPTML_URL . 'assets/img/logo.svg',
1232 11
1233 );
1234 }
1235
1236 /**
1237 * Add menu icon style.
1238 *
1239 * @return void
1240 */
1241 public function menu_icon_style() {
1242 $conflicts_count = $this->get_active_notices_count();
1243 $badge_html = '';
1244
1245 if ( $conflicts_count > 0 ) {
1246 $badge_html = sprintf(
1247 '<style>
1248 #toplevel_page_optimole .wp-menu-name::after {
1249 content: "%d";
1250 background: #d63638;
1251 border-radius: 10px;
1252 padding: 1.5px 1px;
1253 font-size: 11px;
1254 margin-left: 5px;
1255 min-width: 18px;
1256 display: inline-block;
1257 text-align: center;
1258 }
1259 </style>',
1260 $conflicts_count
1261 );
1262 }
1263
1264 echo '<style>#toplevel_page_optimole img{ max-width:22px;padding-top:6px!important;opacity:.9!important;} #toplevel_page_optimole li.wp-first-item{ display:none }</style>' . $badge_html;
1265 }
1266
1267 /**
1268 * Add the settings page.
1269 *
1270 * We do it with another priority as it goes above the cloud library otherwise.
1271 */
1272 public function add_settings_subpage() {
1273 if ( defined( 'OPTIOMLE_HIDE_ADMIN_AREA' ) && OPTIOMLE_HIDE_ADMIN_AREA ) {
1274 return;
1275 }
1276
1277 if ( ! $this->settings->is_connected() ) {
1278 return;
1279 }
1280
1281 add_submenu_page(
1282 'optimole',
1283 __( 'Settings', 'optimole-wp' ),
1284 __( 'Settings', 'optimole-wp' ),
1285 'manage_options',
1286 'optimole#settings',
1287 [ $this, 'render_dashboard_page' ]
1288 );
1289 }
1290
1291
1292 /**
1293 * Redirect old dashboard.
1294 *
1295 * @return void
1296 */
1297 public function redirect_old_dashboard() {
1298 if ( ! is_admin() ) {
1299 return;
1300 }
1301
1302 global $pagenow;
1303
1304 if ( $pagenow !== 'upload.php' ) {
1305 return;
1306 }
1307
1308 if ( ! isset( $_GET['page'] ) ) {
1309 return;
1310 }
1311
1312 if ( $_GET['page'] !== 'optimole' ) {
1313 return;
1314 }
1315
1316 wp_safe_redirect( admin_url( 'admin.php?page=optimole' ) );
1317
1318 exit;
1319 }
1320
1321 /**
1322 * Render dashboard page.
1323 */
1324 public function render_dashboard_page() {
1325 if ( get_option( 'optml_notice_optin', 'no' ) !== 'yes' ) {
1326 update_option( 'optml_notice_optin', 'yes' );
1327 }
1328 ?>
1329 <style>
1330 .notice:not(.optml-notice-optin) {
1331 display: none !important;
1332 }
1333 </style>
1334 <div id="optimole-app"></div>
1335 <?php
1336 }
1337
1338 /**
1339 * Enqueue scripts needed for admin functionality.
1340 *
1341 * @codeCoverageIgnore
1342 */
1343 public function enqueue() {
1344
1345 $current_screen = get_current_screen();
1346 if ( ! isset( $current_screen->id ) ) {
1347 return;
1348 }
1349 if ( $current_screen->id === 'upload' ) {
1350 wp_enqueue_script( OPTML_NAMESPACE . '-media-admin', OPTML_URL . 'assets/js/media.js', [], OPTML_VERSION );
1351 wp_localize_script(
1352 OPTML_NAMESPACE . '-media-admin',
1353 'optimoleMediaListing',
1354 [
1355 'rest_url' => rest_url( OPTML_NAMESPACE . '/v1/move_image' ),
1356 'nonce' => wp_create_nonce( 'wp_rest' ),
1357 ]
1358 );
1359
1360 wp_register_style( 'optml_media_admin_style', false );
1361 wp_add_inline_style(
1362 'optml_media_admin_style',
1363 '
1364 .is-loading {
1365 pointer-events: none;
1366 opacity: 0.5;
1367 display: inline-block;
1368 }
1369 .spinner {
1370 margin-top: 0px;
1371 }
1372 '
1373 );
1374 wp_enqueue_style( 'optml_media_admin_style' );
1375 }
1376 if ( $current_screen->id !== 'toplevel_page_optimole' ) {
1377 return;
1378 }
1379
1380 $asset_file = include OPTML_PATH . 'assets/build/dashboard/index.asset.php';
1381 wp_register_script(
1382 OPTML_NAMESPACE . '-admin',
1383 OPTML_URL . 'assets/build/dashboard/index.js',
1384 array_merge( $asset_file['dependencies'], [ 'updates' ] ),
1385 $asset_file['version'],
1386 true
1387 );
1388
1389 wp_localize_script( OPTML_NAMESPACE . '-admin', 'optimoleDashboardApp', $this->localize_dashboard_app() );
1390 wp_enqueue_script( OPTML_NAMESPACE . '-admin' );
1391
1392 wp_enqueue_style(
1393 OPTML_NAMESPACE . '-admin',
1394 OPTML_URL . 'assets/build/dashboard/style-index.css',
1395 [
1396 'wp-components',
1397 ],
1398 $asset_file['version']
1399 );
1400
1401 do_action( 'themeisle_internal_page', OPTML_PRODUCT_SLUG, 'dashboard' );
1402 }
1403
1404 /**
1405 * Localize the dashboard app.
1406 *
1407 * @codeCoverageIgnore
1408 * @return array
1409 */
1410 private function localize_dashboard_app() {
1411
1412 global $wp_version;
1413 $is_offload_media_available = 'no';
1414 if ( version_compare( $wp_version, '5.3', '>=' ) ) {
1415 $is_offload_media_available = 'yes';
1416 }
1417 $api_key = $this->settings->get( 'api_key' );
1418 $service_data = $this->settings->get( 'service_data' );
1419 if ( ! is_array( $service_data ) ) {
1420 $service_data = [];
1421 }
1422 $user = get_userdata( get_current_user_id() );
1423 $user_status = 'inactive';
1424 $auto_connect = get_option( Optml_Settings::OPTML_USER_EMAIL, 'no' );
1425 $available_apps = isset( $service_data['available_apps'] ) ? $service_data['available_apps'] : null;
1426 if ( isset( $service_data['cdn_key'] ) && $available_apps !== null ) {
1427 foreach ( $service_data['available_apps'] as $app ) {
1428 if ( isset( $app['key'] ) && $app['key'] === $service_data['cdn_key'] && isset( $app['status'] ) && $app['status'] === 'active' ) {
1429 $user_status = 'active';
1430 }
1431 }
1432 }
1433 $routes = [];
1434 foreach ( Optml_Rest::$rest_routes as $route_type ) {
1435 foreach ( $route_type as $route => $details ) {
1436 $routes[ $route ] = OPTML_NAMESPACE . '/v1/' . $route;
1437 }
1438 }
1439 // Disable this for now, we need a better way to detect it.
1440 // $cron_disabled = apply_filters( 'optml_offload_wp_cron_disabled', defined( 'DISABLE_WP_CRON' ) && constant( 'DISABLE_WP_CRON' ) === true );
1441
1442 $language = get_user_locale();
1443 $available_languages = [
1444 'de_DE' => 'de',
1445 'de_DE_formal' => 'de',
1446 ];
1447 $lang_code = isset( $available_languages[ $language ] ) ? 'de' : 'en';
1448
1449 $site_settings = $this->settings->get_site_settings();
1450 if ( ! empty( $service_data ) ) {
1451 $service_data['domain_dns'] = $this->settings->get_cdn_url();
1452 }
1453 if ( isset( $service_data['renews_on'] ) ) {
1454 $service_data['renews_on_formatted'] = date_i18n( get_option( 'date_format' ), $service_data['renews_on'] );
1455
1456 }
1457
1458 $user_plan = isset( $service_data['plan'] ) ? $service_data['plan'] : 'free';
1459
1460 return [
1461 'strings' => $this->get_dashboard_strings(),
1462 'assets_url' => OPTML_URL . 'assets/',
1463 'dam_url' => 'admin.php?page=optimole-dam',
1464 'connection_status' => empty( $service_data ) ? 'no' : 'yes',
1465 'has_application' => isset( $service_data['app_count'] ) && $service_data['app_count'] >= 1 ? 'yes' : 'no',
1466 'user_status' => $user_status,
1467 'available_apps' => $available_apps,
1468 'api_key' => $api_key,
1469 'routes' => $routes,
1470 'language' => $lang_code,
1471 'nonce' => wp_create_nonce( 'wp_rest' ),
1472 'user_data' => $service_data,
1473 'remove_latest_images' => defined( 'OPTML_REMOVE_LATEST_IMAGES' ) && constant( 'OPTML_REMOVE_LATEST_IMAGES' ) ? ( OPTML_REMOVE_LATEST_IMAGES ? 'yes' : 'no' ) : 'no',
1474 'current_user' => [
1475 'email' => $user->user_email,
1476 ],
1477 'site_settings' => $site_settings,
1478 'offload_limit' => $this->settings->get( 'offload_limit' ),
1479 'home_url' => home_url(),
1480 'optimoleHome' => tsdk_translate_link( 'https://optimole.com/' ),
1481 'optimoleDashHome' => tsdk_translate_link( 'https://dashboard.optimole.com/', 'query' ),
1482 'optimoleDashBilling' => tsdk_translate_link( 'https://dashboard.optimole.com/settings/billing', 'query' ),
1483 'optimoleDashMetrics' => tsdk_translate_link( tsdk_utmify( 'https://dashboard.optimole.com/metrics', 'wp-plugin', 'shortcut' ) ),
1484 'offload_upgrade_url' => tsdk_translate_link( tsdk_utmify( 'https://optimole.com/pricing/', 'offload' ) ),
1485 'days_since_install' => round( ( time() - get_option( 'optimole_wp_install', 0 ) ) / DAY_IN_SECONDS ),
1486 'is_offload_media_available' => $is_offload_media_available,
1487 'auto_connect' => $auto_connect,
1488 'cron_disabled' => false, // $cron_disabled && ! function_exists( 'as_schedule_single_action' ),
1489 'submenu_links' => [
1490 [
1491 'href' => 'admin.php?page=optimole#settings',
1492 'text' => __( 'Settings', 'optimole-wp' ),
1493 'hash' => '#settings',
1494 ],
1495 ],
1496 'bf_notices' => $this->get_bf_notices( $user_plan ),
1497 'spc_banner' => $this->get_spc_banner(),
1498 'show_exceed_plan_quota_notice' => $this->should_show_exceed_quota_warning(),
1499 'show_free_user_with_offload_notice' => get_option( 'optml_has_offloading_enabled_on_upgrade', 'no' ),
1500 'report_issue_url' => add_query_arg(
1501 [
1502 'utm_source' => 'plugin',
1503 'utm_medium' => 'wp_dashboard',
1504 'utm_campaign' => 'report_issue',
1505 'contact_website' => home_url(),
1506 ],
1507 self::get_contact_base_link()
1508 ),
1509 'upgrade_url' => esc_url( self::get_upgrade_base_link() ),
1510 ];
1511 }
1512
1513 /**
1514 * Get the SPC banner data.
1515 *
1516 * @return array|null
1517 */
1518 private function get_spc_banner() {
1519 // User can't dismiss notice or install plugins.
1520 if ( ! current_user_can( 'manage_options' ) || ! current_user_can( 'install_plugins' ) ) {
1521 return null;
1522 }
1523
1524 // User has dismissed the notice.
1525 if ( get_option( self::SPC_BANNER_DISMISS_KEY ) === 'yes' ) {
1526 return null;
1527 }
1528
1529 // User has installed the pro plugin.
1530 if ( defined( 'SPC_PRO_PATH' ) ) {
1531 return null;
1532 }
1533
1534 // User has installed the plugin.
1535 if ( defined( 'SPC_PATH' ) ) {
1536 return null;
1537 }
1538
1539 return [
1540 'activate_url' => $this->get_spc_activate_url(),
1541 'status' => $this->get_spc_status(),
1542 'banner_dismiss_key' => self::SPC_BANNER_DISMISS_KEY,
1543 'i18n' => [
1544 'dismiss' => __( 'Dismiss', 'optimole-wp' ),
1545 'title' => __( 'Pair Optimole with Super Page Cache', 'optimole-wp' ),
1546 'byline' => __( 'Improve your Core Web Vitals with our recommended caching solution', 'optimole-wp' ),
1547 'features' => [
1548 __( 'Edge Caching (with Cloudflare free plan)', 'optimole-wp' ),
1549 __( 'Works with Optimole optimization', 'optimole-wp' ),
1550 __( 'Lightning Speed Disk Caching', 'optimole-wp' ),
1551 ],
1552 'cta' => __( 'Get Super Page Cache', 'optimole-wp' ),
1553 'activate' => __( 'Activate Super Page Cache', 'optimole-wp' ),
1554 'installing' => __( 'Installing Super Page Cache...', 'optimole-wp' ),
1555 'activating' => __( 'Activating Super Page Cache...', 'optimole-wp' ),
1556 'activated' => __( 'Super Page Cache is active!', 'optimole-wp' ),
1557 'error' => __( 'Something went wrong. Please refresh the page and try again.', 'optimole-wp' ),
1558 ],
1559 ];
1560 }
1561
1562 /**
1563 * Get the SPC activate URL.
1564 *
1565 * @return string
1566 */
1567 private function get_spc_activate_url() {
1568 return add_query_arg(
1569 [
1570 'plugin_status' => 'all',
1571 'paged' => 1,
1572 'action' => 'activate',
1573 'plugin' => rawurlencode( 'wp-cloudflare-page-cache/wp-cloudflare-super-page-cache.php' ),
1574 '_wpnonce' => wp_create_nonce( 'activate-plugin_wp-cloudflare-page-cache/wp-cloudflare-super-page-cache.php' ),
1575 ],
1576 admin_url( 'plugins.php' )
1577 );
1578 }
1579
1580 /**
1581 * Get the SPC status.
1582 *
1583 * @return string
1584 */
1585 private function get_spc_status() {
1586 if ( is_plugin_active( 'wp-cloudflare-page-cache/wp-cloudflare-super-page-cache.php' ) ) {
1587 return 'active';
1588 }
1589
1590 if ( file_exists( WP_PLUGIN_DIR . '/wp-cloudflare-page-cache/wp-cloudflare-super-page-cache.php' ) ) {
1591 return 'installed';
1592 }
1593
1594 return 'not-installed';
1595 }
1596
1597 /**
1598 * Get the black friday notices.
1599 *
1600 * @param non-empty-string $user_plan User plan.
1601 *
1602 * @return array<string, mixed> Notice data.
1603 */
1604 public function get_bf_notices( $user_plan ) {
1605
1606 $notices = [];
1607 $has_free_plan = 'free' === $user_plan;
1608
1609 if ( ! $has_free_plan ) {
1610 return $notices;
1611 }
1612
1613 /**
1614 * The black friday sale period flag.
1615 *
1616 * @var bool $is_black_friday Whether is black friday sale period or not.
1617 */
1618 $is_black_friday = apply_filters( 'themeisle_sdk_is_black_friday_sale', false );
1619
1620 if ( ! $is_black_friday ) {
1621 return $notices;
1622 }
1623
1624 /**
1625 * The current date.
1626 *
1627 * @var DateTime $now Current date.
1628 */
1629 $now = apply_filters( 'themeisle_sdk_current_date', new \DateTime( 'now' ) );
1630 $current_year = $now->format( 'Y' );
1631
1632 $black_friday_day = new \DateTime( "last Friday of November $current_year" );
1633 $sale_start = clone $black_friday_day;
1634 $sale_start->modify( 'monday this week' );
1635 $sale_start->setTime( 0, 0 );
1636
1637 $end = clone $sale_start;
1638 $end->modify( '+7 days' );
1639 $end->setTime( 23, 59, 59 );
1640
1641 $promo_code = 'BFCM2525';
1642
1643 $notices['sidebar'] = [
1644 'title' => sprintf(
1645 '<span class="text-promo-orange">%1$s:</span> %2$s - %3$s',
1646 __( 'Private Sale', 'optimole-wp' ),
1647 wp_date( 'j M', $sale_start->getTimestamp() ),
1648 wp_date( 'j M', $end->getTimestamp() )
1649 ),
1650 'subtitle' => sprintf(
1651 /* translators: %1$s is the promo code, %2$s is the discount amount ('25 off') */
1652 __( 'Use code %1$s for %2$s on yearly plans.', 'optimole-wp' ),
1653 '<span class="border-b border-0 border-white border-dashed text-promo-orange">' . $promo_code . '</span>',
1654 '<span class="text-promo-orange uppercase">' . __( '25% discount', 'optimole-wp' ) . '</span>'
1655 ),
1656 'cta_link' => esc_url_raw( tsdk_utmify( tsdk_translate_link( self::get_upgrade_base_link() ), 'bfcm25', 'sidebarnotice' ) ),
1657 ];
1658
1659 if ( get_option( self::BF_PROMO_DISMISS_KEY ) === 'yes' ) {
1660 return $notices;
1661 }
1662
1663 $message = sprintf(
1664 // translators: %1$s is the promo code, %2$s is the discount amount ('25% off')
1665 __( 'Use coupon code %1$s for an instant %2$s on Optimole yearly plans', 'optimole-wp' ),
1666 '<span class="border-b border-0 border-white border-dashed text-promo-orange">' . $promo_code . '</span>',
1667 '<span class="text-promo-orange uppercase">' . __( '25% discount', 'optimole-wp' ) . '</span>'
1668 );
1669 $cta_link = esc_url_raw( tsdk_utmify( self::get_upgrade_base_link(), 'bfcm25', 'notice' ) );
1670 $cta_text = __( 'Claim now', 'optimole-wp' );
1671
1672 $notices['banner'] = [
1673 /* translators: number of days left */
1674 'urgency' => sprintf( __( 'Hurry up! only %s left', 'optimole-wp' ), human_time_diff( $end->getTimestamp(), $now->getTimestamp() ) ),
1675 /* translators: private sale */
1676 'title' => sprintf( __( 'Black Friday %s', 'optimole-wp' ), '<span class="text-promo-orange">' . __( 'private sale', 'optimole-wp' ) . '</span>' ),
1677 'subtitle' => $message,
1678 'cta_text' => $cta_text,
1679 'cta_link' => $cta_link,
1680 'dismiss_key' => self::BF_PROMO_DISMISS_KEY,
1681 ];
1682
1683 return $notices;
1684 }
1685
1686 /**
1687 * Get all dashboard strings.
1688 *
1689 * @codeCoverageIgnore
1690 * @return array
1691 */
1692 private function get_dashboard_strings() {
1693 return [
1694 'optimole' => 'Optimole',
1695 'version' => OPTML_VERSION,
1696 'terms_menu' => __( 'Terms', 'optimole-wp' ),
1697 'privacy_menu' => __( 'Privacy', 'optimole-wp' ),
1698 'testdrive_menu' => __( 'Test Optimole', 'optimole-wp' ),
1699 'service_details' => __( 'Image optimization service', 'optimole-wp' ),
1700 'dashboard_title' => __( 'Image Optimization Overview', 'optimole-wp' ),
1701 'banner_title' => __( 'All images are automatically optimized!', 'optimole-wp' ),
1702 'banner_description' => __( 'Optimole is handling all your images in real-time with our CloudFront CDN (450+ locations worldwide)', 'optimole-wp' ),
1703 'quick_action_title' => __( 'Quick Actions', 'optimole-wp' ),
1704 'connect_btn' => __( 'Connect to Optimole', 'optimole-wp' ),
1705 'disconnect_btn' => __( 'Disconnect', 'optimole-wp' ),
1706 'select' => __( 'Select', 'optimole-wp' ),
1707 'your_domain' => __( 'your domain', 'optimole-wp' ),
1708 'add_api' => __( 'Add your API Key', 'optimole-wp' ),
1709 'your_api_key' => __( 'Your API Key', 'optimole-wp' ),
1710 'looking_for_api_key' => __( 'LOOKING FOR YOUR API KEY?', 'optimole-wp' ),
1711 'refresh_stats_cta' => __( 'Refresh Stats', 'optimole-wp' ),
1712 'updating_stats_cta' => __( 'UPDATING STATS', 'optimole-wp' ),
1713 'api_key_placeholder' => __( 'API Key', 'optimole-wp' ),
1714 'account_needed_heading' => __( 'Supercharge Your WordPress Images in 60 Seconds', 'optimole-wp' ),
1715 'account_needed_sub_heading' => __( 'Stop sacrificing image quality for page speed. Optimole delivers both.', 'optimole-wp' ),
1716 'account_needed_trust_badge' => __( 'TRUSTED BY 200,000+ HAPPY USERS', 'optimole-wp' ),
1717 'account_needed_setup_time' => __( 'Setup is instant - just click connect', 'optimole-wp' ),
1718 'account_needed_benefits_toggle' => __( 'See what you\'ll get', 'optimole-wp' ),
1719 'invalid_key' => __( 'Invalid API Key', 'optimole-wp' ),
1720 'keep_connected' => __( 'Ok, keep me connected', 'optimole-wp' ),
1721 'cloud_library' => __( 'Cloud Library', 'optimole-wp' ),
1722 'image_storage' => __( 'Image Storage', 'optimole-wp' ),
1723 'word' => __( 'word', 'optimole-wp' ),
1724 'path' => __( 'path', 'optimole-wp' ),
1725 'disconnect_title' => __( 'You are about to disconnect from the Optimole API', 'optimole-wp' ),
1726 'disconnect_desc' => __(
1727 'Please note that disconnecting your site from the Optimole API will impact your website performance.
1728 If you still want to disconnect click the button below.',
1729 'optimole-wp'
1730 ),
1731 'email_address_label' => __( 'Your email address', 'optimole-wp' ),
1732 'steps_connect_api_title' => __( 'Connect your account', 'optimole-wp' ),
1733 'register_btn' => __( 'Create & connect your account', 'optimole-wp' ),
1734 'secure_connection' => __( 'Secure connection - your data is protected', 'optimole-wp' ),
1735 'step_one_api_title' => __( 'Enter your API key.', 'optimole-wp' ),
1736 'optml_dashboard' => sprintf(
1737 /* translators: 1 is the opening anchor tag, 2 is the closing anchor tag */
1738 __( 'Get it from the %1$s Optimole Dashboard%2$s.', 'optimole-wp' ),
1739 '<a style="white-space:nowrap; text-decoration: underline !important;" href="' . esc_url( tsdk_translate_link( 'https://dashboard.optimole.com/', 'query' ) ) . '" target="_blank"> ',
1740 '<span style="text-decoration:none; font-size:15px; margin-top:2px;" class="dashicons dashicons-external"></span></a>'
1741 ),
1742 'steps_connect_api_desc' => sprintf(
1743 /* translators: 1 is the opening anchor tag, 2 is the closing anchor tag */
1744 __( '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. <br/>', 'optimole-wp' ),
1745 '<a href="' . esc_url( tsdk_translate_link( 'https://dashboard.optimole.com/', 'query' ) ) . '" target="_blank"> ',
1746 '</a>'
1747 ),
1748 'api_exists' => __( 'I already have an API key.', 'optimole-wp' ),
1749 'back_to_register' => __( 'Register account', 'optimole-wp' ),
1750 'back_to_connect' => __( 'Go to previous step', 'optimole-wp' ),
1751 /* translators: 1 is starting anchor tag, 2 is ending anchor tag */
1752 'error_register' => sprintf( __( 'Error registering account. You can try again %1$shere%2$s', 'optimole-wp' ), '<a href="' . esc_url( tsdk_translate_link( 'https://dashboard.optimole.com/register', 'query' ) ) . '" target="_blank"> ', '</a>' ),
1753 'invalid_email' => __( 'Please use a valid email address.', 'optimole-wp' ),
1754 'connected' => __( 'CONNECTED', 'optimole-wp' ),
1755 'connecting' => __( 'CONNECTING', 'optimole-wp' ),
1756 'not_connected' => __( 'NOT CONNECTED', 'optimole-wp' ),
1757 'usage' => __( 'Monthly Usage', 'optimole-wp' ),
1758 'quota' => __( 'Monthly visits:', 'optimole-wp' ),
1759 'tooltip_visits_title' => __( 'What are visits?', 'optimole-wp' ),
1760 /* translators: 1 is the day when the visits reset, for example 1st of each month */
1761 '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' ),
1762 'logged_in_as' => __( 'LOGGED IN AS', 'optimole-wp' ),
1763 'private_cdn_url' => __( 'IMAGES DOMAIN', 'optimole-wp' ),
1764 'existing_user' => __( 'Existing user?', 'optimole-wp' ),
1765 'notification_message_register' => __( 'We sent you the API Key in the email. Add it below to connect to Optimole.', 'optimole-wp' ),
1766 'premium_support' => __( 'Access our Premium Support', 'optimole-wp' ),
1767 'account_needed_title' => sprintf(
1768 /* translators: 1 is the link to optimole.com */
1769 __( 'Connect to Optimole\'s powerful image optimization service with a free API key from %s.', 'optimole-wp' ),
1770 ' <a href="' . esc_url( tsdk_translate_link( 'https://dashboard.optimole.com/register', 'query' ) ) . '" target="_blank">optimole.com</a>'
1771 ),
1772 'account_needed_subtitle_1' => sprintf(
1773 /* 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. */
1774 __( '%1$sOptimize unlimited images%2$s for up to %3$s monthly %4$svisitors%5$s - completely FREE.', 'optimole-wp' ),
1775 '<strong>',
1776 '</strong>',
1777 number_format_i18n( 2000 ),
1778 '<a href="https://docs.optimole.com/article/1134-how-optimole-counts-the-number-of-visitors" target="_blank">',
1779 '</a>'
1780 ),
1781 'account_needed_subtitle_3' => sprintf(
1782 /* translators: 1 is the starting anchor tag for the docs link, 2 is the ending anchor tag. */
1783 __( 'Need help? %1$sGetting Started with Optimole%2$s', 'optimole-wp' ),
1784 '<a target="_blank" href="https://docs.optimole.com/article/1173-how-to-get-started-with-optimole-in-just-3-steps">',
1785 '</a>'
1786 ),
1787 'account_needed_subtitle_2' => sprintf(
1788 /* translators: 1 is the starting bold tag, 2 is the ending bold tag */
1789 __(
1790 '%1$sInstant global delivery%2$s with CloudFront CDN - your images load 2-3x faster worldwide from 450+ locations.',
1791 'optimole-wp'
1792 ),
1793 '<strong>',
1794 '</strong>'
1795 ),
1796 'account_needed_subtitle_4' => sprintf(
1797 /* translators: 1 is the starting bold tag, 2 is the ending bold tag */
1798 __(
1799 '%1$sAdaptive optimization%2$s that perfectly sizes images for every visitor\'s device and connection speed.',
1800 'optimole-wp'
1801 ),
1802 '<strong>',
1803 '</strong>'
1804 ),
1805 'account_needed_footer' => sprintf(
1806 /* translators: 1 is the number of users using Optimole */
1807 __( 'Trusted by more than %1$s happy users', 'optimole-wp' ),
1808 number_format_i18n( 200000, 0 )
1809 ),
1810 'account_connecting_title' => __( 'Connecting to Optimole', 'optimole-wp' ),
1811 'account_connecting_subtitle' => __( 'Sit tight while we connect you to the Dashboard', 'optimole-wp' ),
1812 'notice_just_activated' => ! $this->settings->is_connected() ?
1813 sprintf(
1814 /* translators: 1 starting bold tag, 2 is the ending bold tag */
1815 __( '%1$sImage optimisation is currently running.%2$s <br/> 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' ),
1816 '<strong>',
1817 '</strong>'
1818 )
1819 : '',
1820 'notice_api_not_working' => __(
1821 '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.<br/>
1822 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.',
1823 'optimole-wp'
1824 ),
1825 'notice_disabled_account' => sprintf(
1826 /* 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 */
1827 __( '%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' ),
1828 '<b><a href="' . esc_url( tsdk_translate_link( self::get_upgrade_base_link() ) ) . '">',
1829 '</a></b>',
1830 '<b>',
1831 '</b>',
1832 '<br>'
1833 ),
1834 'signup_terms' => sprintf(
1835 /* translators: 1 is starting anchor tag to terms, 2 is starting anchor tag to privacy link and 3 is ending anchor tag. */
1836 __( 'By signing up, you agree to our %1$sTerms of Service %3$s and %2$sPrivacy Policy %3$s.', 'optimole-wp' ),
1837 '<a href="' . esc_url( tsdk_translate_link( 'https://optimole.com/terms/' ) ) . '" target="_blank" >',
1838 '<a href="' . esc_url( tsdk_translate_link( 'https://optimole.com/privacy-policy/' ) ) . '" target="_blank">',
1839 '</a>'
1840 ),
1841 'dashboard_menu_item' => __( 'Dashboard', 'optimole-wp' ),
1842 'settings_menu_item' => __( 'Settings', 'optimole-wp' ),
1843 'help_menu_item' => __( 'Help', 'optimole-wp' ),
1844 'settings_watermark_menu_item' => __( 'Watermark', 'optimole-wp' ),
1845 'settings_exclusions_menu_item' => __( 'Exclusions', 'optimole-wp' ),
1846 'settings_resize_menu_item' => __( 'Resize', 'optimole-wp' ),
1847 'settings_compression_menu_item' => __( 'Compression', 'optimole-wp' ),
1848 'advanced_settings_menu_item' => __( 'Advanced', 'optimole-wp' ),
1849 'general_settings_menu_item' => __( 'General', 'optimole-wp' ),
1850 'lazyload_settings_menu_item' => __( 'Lazyload', 'optimole-wp' ),
1851 'watermarks_menu_item' => __( 'Watermark', 'optimole-wp' ),
1852 'conflicts_menu_item' => __( 'Possible Issues', 'optimole-wp' ),
1853 'conflicts' => [
1854 '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' ),
1855 'message' => __( 'Details', 'optimole-wp' ),
1856 'conflict_close' => __( 'I\'ve done this.', 'optimole-wp' ),
1857 'no_conflicts_found' => __( 'No conflicts found. We are all peachy now. 🍑', 'optimole-wp' ),
1858 ],
1859 'upgrade' => [
1860 'title' => __( 'Upgrade', 'optimole-wp' ),
1861 'title_long' => __( 'Upgrade to Optimole Pro', 'optimole-wp' ),
1862 'reason_1' => __( 'Priority & Live Chat support', 'optimole-wp' ),
1863 'reason_2' => __( 'Extend visits limit', 'optimole-wp' ),
1864 'reason_3' => __( 'Custom domain', 'optimole-wp' ),
1865 'reason_4' => __( 'Site audit', 'optimole-wp' ),
1866 'cta' => __( 'View plans', 'optimole-wp' ),
1867 ],
1868 'neve' => [
1869 'is_active' => defined( 'NEVE_VERSION' ) ? 'yes' : 'no',
1870 'byline' => __( 'Fast, perfomance built-in WordPress theme.', 'optimole-wp' ),
1871 'reason_1' => __( 'Lightweight, 25kB in page-weight.', 'optimole-wp' ),
1872 'reason_2' => __( '100+ Starter Sites available.', 'optimole-wp' ),
1873 'reason_3' => __( 'AMP/Mobile ready.', 'optimole-wp' ),
1874 'reason_4' => __( 'Lots of customizations options.', 'optimole-wp' ),
1875 'reason_5' => __( 'Fully compatible with Optimole.', 'optimole-wp' ),
1876 ],
1877 'metrics' => [
1878 'metricsTitle1' => __( 'Images optimized', 'optimole-wp' ),
1879 'metricsSubtitle1' => __( 'Since plugin activation', 'optimole-wp' ),
1880 'metricsTitle2' => __( 'Saved File Size', 'optimole-wp' ),
1881 'metricsSubtitle2' => __( 'Latest 10 images', 'optimole-wp' ),
1882 'metricsTitle3' => __( 'Average Compression', 'optimole-wp' ),
1883 'metricsSubtitle3' => __( 'Average Reduction', 'optimole-wp' ),
1884 'metricsTitle4' => __( 'CDN Traffic', 'optimole-wp' ),
1885 'metricsSubtitle4' => __( 'This month', 'optimole-wp' ),
1886 'metricsTitle5' => __( 'Offloaded images', 'optimole-wp' ),
1887 'metricsSubtitle5' => __( 'Offloaded to cloud', 'optimole-wp' ),
1888 // translators: is used as singular when the number of images is 1 for a unit label.
1889 'image' => __( 'image', 'optimole-wp' ),
1890 // translators: is used as plural when the number of images is more than 1 for a unit label.
1891 'images' => __( 'images', 'optimole-wp' ),
1892 'view_analytics' => __( 'View Analytics', 'optimole-wp' ),
1893 'adjust_compression' => __( 'Adjust Compression', 'optimole-wp' ),
1894 'manage_offloading' => __( 'Manage image offloading', 'optimole-wp' ),
1895 ],
1896 'quick_actions' => [
1897 'speed_test_title' => __( 'Test Your Site Speed', 'optimole-wp' ),
1898 'speed_test_desc' => __( 'Run speed test', 'optimole-wp' ),
1899 'speed_test_link' => add_query_arg( 'url', get_site_url(), 'https://pagespeed.web.dev/analysis' ),
1900 'clear_cache_images' => __( 'Clear Cached Images', 'optimole-wp' ),
1901 'clear_cache' => __( 'Clear cache', 'optimole-wp' ),
1902 'offload_images' => __( 'Enable Offload Images', 'optimole-wp' ),
1903 'offload_images_desc' => __( 'Free up space on your server', 'optimole-wp' ),
1904 'advance_settings' => __( 'Advanced Settings', 'optimole-wp' ),
1905 'configure_settings' => __( 'Configure settings', 'optimole-wp' ),
1906 ],
1907 'options_strings' => [
1908 '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' ),
1909 'watermark_media_title' => __( 'Watermarks', 'optimole-wp' ),
1910 'learn_more' => __( 'Learn more', 'optimole-wp' ),
1911 'open_optimole_dashboard' => __( 'Open Optimole Dashboard', 'optimole-wp' ),
1912 'watermark_footer' => __( 'Watermark rules and exclusions are fully managed in your Optimole Dashboard.', 'optimole-wp' ),
1913 'watermark_info_1' => __( 'Choose from multiple watermark positions and set opacity.', 'optimole-wp' ),
1914 'watermark_info_2' => __( 'Set different watermarks for different image sizes.', 'optimole-wp' ),
1915 'watermark_info_3' => __( 'Create rule-based targeting by filename, page URL, class, and more.', 'optimole-wp' ),
1916 'compression_mode' => __( 'Compression Mode', 'optimole-wp' ),
1917 'compression_mode_speed_optimized' => __( 'Speed Optimized', 'optimole-wp' ),
1918 'compression_mode_quality_optimized' => __( 'Quality Optimized', 'optimole-wp' ),
1919 'compression_mode_custom' => __( 'Custom', 'optimole-wp' ),
1920 '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' ),
1921 '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' ),
1922 'compression_mode_custom_desc' => __( 'Customize each setting individually for complete control over image compression.', 'optimole-wp' ),
1923
1924 'best_format_title' => __( 'Automatic Best Image Format Selection', 'optimole-wp' ),
1925 'best_format_desc' => sprintf(
1926 /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */
1927 __( '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' ),
1928 '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1942-best-format">',
1929 '</a>'
1930 ),
1931 'add_filter' => __( 'Add filter', 'optimole-wp' ),
1932 'add_site' => __( 'Add site', 'optimole-wp' ),
1933 'admin_bar_desc' => __( 'Show in the WordPress admin bar the available quota from Optimole service.', 'optimole-wp' ),
1934 'auto_q_title' => __( 'Auto', 'optimole-wp' ),
1935 'cache_desc' => sprintf(
1936 /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */
1937 __( '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' ),
1938 '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1941-clear-cached-resources">',
1939 '</a>'
1940 ),
1941 'cache_title' => __( 'Clear Cached Resources', 'optimole-wp' ),
1942 'clear_cache_notice' => __( 'Clearing cached resources will re-optimize the images and might affect the site performance for a few minutes.', 'optimole-wp' ),
1943 'image_size_notice' => sprintf(
1944 /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */
1945 __( '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' ),
1946 '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1947-add-new-image-crop-size">',
1947 '</a>'
1948 ),
1949 'clear_cache_images' => __( 'Clear cached images', 'optimole-wp' ),
1950 'clear_cache_assets' => __( 'Clear cached CSS & JS', 'optimole-wp' ),
1951 'connect_step_0' => __( 'Connecting your site to the Optimole service.', 'optimole-wp' ),
1952 'connect_step_1' => __( 'Checking for possible conflicts.', 'optimole-wp' ),
1953 'connect_step_2' => __( 'Inspecting the images from your site.', 'optimole-wp' ),
1954 'connect_step_3' => __( 'All done, Optimole is currently optimizing your site.', 'optimole-wp' ),
1955 'disabled' => __( 'Disabled', 'optimole-wp' ),
1956 'enable_avif_title' => __( 'AVIF Image Support', 'optimole-wp' ),
1957 'enable_avif_desc' => sprintf(
1958 /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */
1959 __( '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' ),
1960 '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1943-avif-conversion">',
1961 '</a>'
1962 ),
1963 'enable_bg_lazyload_desc' => sprintf(
1964 /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */
1965 __( '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' ),
1966 '<a class="inline-block text-purple-gray underline" target="_blank" href="https://docs.optimole.com/article/1169-how-to-enable-the-background-lazyload-feature-for-certain-background-images">',
1967 '</a>'
1968 ),
1969 'enable_bg_lazyload_title' => __( 'CSS Background Lazy Load', 'optimole-wp' ),
1970 'enable_video_lazyload_desc' => __( 'Lazy load embedded videos and iframe content', 'optimole-wp' ),
1971 'enable_video_lazyload_title' => __( 'Video & iframes', 'optimole-wp' ),
1972 'enable_noscript_desc' => sprintf(
1973 /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */
1974 __( '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' ),
1975 '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1959-noscript-tag">',
1976 '</a>'
1977 ),
1978 // translators: %s is the name of the html tag (e.g: <noscript> );
1979 'enable_noscript_title' => sprintf( __( 'Enable %s fallback', 'optimole-wp' ), '<custom_component/>' ),
1980 'enable_gif_replace_title' => __( 'GIF to Video Conversion', 'optimole-wp' ),
1981 'enable_offload_media_title' => __( 'Store Your Images in Optimole Cloud', 'optimole-wp' ),
1982 'enable_offload_media_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Free up space on your server by transferring your images to Optimole Cloud; you can transfer them back anytime. Once moved, the images will still be visible in the Media Library and can be used as before. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1967-store-your-images-in-optimole-cloud">', '</a>' ),
1983 'enable_cloud_images_title' => __( 'Unified Image Access', 'optimole-wp' ),
1984 'enable_cloud_images_desc' => sprintf(
1985 /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */
1986 __( 'Enable this setting to access all your Optimole images, including those from other websites connected to your Optimole account, directly on this site. They will be available for browsing in the Cloud Library tab. %1$sLearn more%2$s', 'optimole-wp' ),
1987 '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1323-cloud-library-browsing">',
1988 '</a>'
1989 ),
1990 'enable_image_replace' => __( 'Enable Optimole Image Handling', 'optimole-wp' ),
1991 'enable_lazyload_placeholder_desc' => sprintf(
1992 /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */
1993 __( 'Enable this to use a generic transparent placeholder instead of the blurry images during lazy loading. %1$sLearn more%2$s', 'optimole-wp' ),
1994 '<a class="inline-block underline text-white" target=”_blank” href="https://docs.optimole.com/article/1192-lazy-load-generic-placeholder">',
1995 '</a>'
1996 ),
1997
1998 'lazyload_behaviour_title' => __( 'Loading Method', 'optimole-wp' ),
1999 'lazyload_behaviour_desc' => sprintf(
2000 /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */
2001 __( 'Choose how Optimole will handle lazy loading for images on your website. %1$sLearn more%2$s', 'optimole-wp' ),
2002 '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1958-lazy-loading-behaviour">',
2003 '</a>'
2004 ),
2005 'lazyload_behaviour_all' => __( 'Lazy Load All Images', 'optimole-wp' ),
2006 'lazyload_behaviour_all_desc' => __( 'All images will use lazy loading regardless of position.', 'optimole-wp' ),
2007 'lazyload_behaviour_viewport' => __( 'Skip images above the fold', 'optimole-wp' ),
2008 'lazyload_behaviour_viewport_desc' => __( 'Automatically detects and immediately loads images visible in the initial viewport. Detection is done with a lightweight client-side script that identifies what\'s visible on each user\'s screen. All other images will lazy load.', 'optimole-wp' ),
2009 'lazyload_behaviour_fixed' => __( 'Skip lazy loading for first images', 'optimole-wp' ),
2010 'lazyload_behaviour_fixed_desc' => __( 'Indicate how many images at the top of each page should bypass lazy loading, ensuring they\'re instantly visible.', 'optimole-wp' ),
2011 'enable_lazyload_placeholder_title' => __( 'Enable generic placeholder color', 'optimole-wp' ),
2012 'enable_network_opt_desc' => sprintf(
2013 /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */
2014 __( 'When enabled, Optimole will automatically reduce the image quality when it detects a slower network, making your images load faster on low-speed internet connections. %1$sLearn more%2$s', 'optimole-wp' ),
2015 '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1945-network-based-optimizations">',
2016 '</a>'
2017 ),
2018 'enable_network_opt_title' => __( 'Network-based Optimizations', 'optimole-wp' ),
2019 'enable_resize_smart_desc' => sprintf(
2020 /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */
2021 __( 'When enabled, Optimole automatically detects the most interesting or important part of your images. When pictures are resized or cropped, this feature ensures the focus stays on the most interesting part of the picture. %1$sLearn more%2$s', 'optimole-wp' ),
2022 '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1871-how-to-use-the-smart-cropping-in-optimole">',
2023 '</a>'
2024 ),
2025 'enable_resize_smart_title' => __( 'Smart Cropping', 'optimole-wp' ),
2026 'enable_retina_desc' => sprintf(
2027 /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */
2028 __( 'Optimizes images for Retina (high-DPI) screens for sharper results. Always enabled for images under 150px wide. %1$sLearn more%2$s', 'optimole-wp' ),
2029 '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1391-what-is-a-retina-image">',
2030 '</a>'
2031 ),
2032 'enable_retina_title' => __( 'Retina Quality', 'optimole-wp' ),
2033 'enable_limit_dimensions_desc' => sprintf(
2034 /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */
2035 __( 'Define the max width or height limits for images on your website. Larger images will be automatically adjusted to fit within these parameters while preserving their original aspect ratio. %1$sLearn more%2$s', 'optimole-wp' ),
2036 '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1946-limit-image-sizes">',
2037 '</a>'
2038 ),
2039 'enable_limit_dimensions_title' => __( 'Limit Image Sizes', 'optimole-wp' ),
2040 'enable_limit_dimensions_notice' => __( 'When you enable this feature to define a max width or height for image resizing, please note that DPR (retina) images will be disabled. This is done to ensure consistency in image dimensions across your website. Although this may result in slightly lower image quality for high-resolution displays, it will help maintain uniform image sizes, improving your website\'s overall layout and potentially boosting performance.', 'optimole-wp' ),
2041 'enable_badge_title' => __( 'Enable Optimole Badge', 'optimole-wp' ),
2042 'enable_badge_settings' => __( 'Badge settings', 'optimole-wp' ),
2043 'enable_badge_show_icon' => __( 'Show only Optimole icon', 'optimole-wp' ),
2044 'enable_badge_position' => __( 'Badge Position', 'optimole-wp' ),
2045 'badge_position_text_1' => __( 'Left', 'optimole-wp' ),
2046 'badge_position_text_2' => __( 'Right', 'optimole-wp' ),
2047 'enable_badge_description' => sprintf(
2048 /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */
2049 __( 'Get 20.000 more visits for free by enabling the Optimole badge on your websites. %1$sLearn more%2$s', 'optimole-wp' ),
2050 '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1940-optimole-badge">',
2051 '</a>'
2052 ),
2053 'image_sizes_title' => __( 'Your cropped image sizes', 'optimole-wp' ),
2054 'enabled' => __( 'Enabled', 'optimole-wp' ),
2055 'exclude_class_desc' => sprintf(
2056 /* translators: 1 is the starting bold tag, 2 is the ending bold tag */
2057 __( '%1$sImage tag%2$s contains class', 'optimole-wp' ),
2058 '<strong>',
2059 '</strong>'
2060 ),
2061 'exclude_ext_desc' => sprintf(
2062 /* translators: 1 is the starting bold tag, 2 is the ending bold tag */
2063 __( '%1$sImage extension%2$s is', 'optimole-wp' ),
2064 '<strong>',
2065 '</strong>'
2066 ),
2067 'exclude_filename_desc' => sprintf(
2068 /* translators: 1 is the starting bold tag, 2 is the ending bold tag */ __( '%1$sImage filename%2$s contains', 'optimole-wp' ),
2069 '<strong>',
2070 '</strong>'
2071 ),
2072 'exclude_desc_optimize' => sprintf(
2073 /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */
2074 __( 'Here you can define exceptions, in case you don\'t want some images to be optimised. %1$sLearn more%2$s', 'optimole-wp' ),
2075 '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1191-exclude-from-optimizing-or-lazy-loading">',
2076 '</a>'
2077 ),
2078 'exclude_title_lazyload' => __( 'Don\'t lazy-load images if', 'optimole-wp' ),
2079 'exclude_desc_lazyload' => sprintf(
2080 /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */
2081 __( 'Define exceptions, in case you don\'t want the lazy-load to be active on certain images. %1$sLearn more%2$s', 'optimole-wp' ),
2082 '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1191-exclude-from-optimizing-or-lazy-loading">',
2083 '</a>'
2084 ),
2085 'exclude_title_optimize' => __( 'Don\'t optimize and don\'t lazy-load images if', 'optimole-wp' ),
2086 'exclude_url_desc' => sprintf(
2087 /* translators: 1 is the starting bold tag, 2 is the ending bold tag */ __( '%1$sPage url%2$s contains', 'optimole-wp' ),
2088 '<strong>',
2089 '</strong>'
2090 ),
2091 'name' => sprintf(
2092 /* translators: 1 is the starting bold tag, 2 is the ending bold tag */ __( '%1$sName: %2$s', 'optimole-wp' ),
2093 '<strong>',
2094 '</strong>'
2095 ),
2096 'cropped' => __( 'cropped', 'optimole-wp' ),
2097 'exclude_url_match_desc' => sprintf(
2098 /* translators: 1 is the starting bold tag, 2 is the ending bold tag */ __( '%1$sPage url%2$s matches', 'optimole-wp' ),
2099 '<strong>',
2100 '</strong>'
2101 ),
2102 'exclude_first' => __( 'Exclude first', 'optimole-wp' ),
2103 'images' => __( 'images', 'optimole-wp' ),
2104 'exclude_first_images_title' => __( 'Bypass Lazy Load for First Images', 'optimole-wp' ),
2105 'exclude_first_images_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Indicate how many images at the top of each page should bypass lazy loading, ensuring they’re instantly visible. Enter 0 to not exclude any images from the lazy loading process. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1948-bypass-lazy-load-for-first-images">', '</a>' ),
2106 'filter_class' => __( 'Image class', 'optimole-wp' ),
2107 'filter_ext' => __( 'Image extension', 'optimole-wp' ),
2108 'filter_filename' => __( 'Image filename', 'optimole-wp' ),
2109 'filter_operator_contains' => __( 'contains', 'optimole-wp' ),
2110 'filter_operator_matches' => __( 'matches', 'optimole-wp' ),
2111 'filter_operator_is' => __( 'is', 'optimole-wp' ),
2112 'filter_url' => __( 'Page URL', 'optimole-wp' ),
2113 'filter_helper' => __( 'For homepage use `home` keyword.', 'optimole-wp' ),
2114 'gif_replacer_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Enable this to automatically convert GIF images to Video files (MP4 and WebM). %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1171-gif-to-video-conversion">', '</a>' ),
2115 'height_field' => __( 'Height', 'optimole-wp' ),
2116 'add_image_size_button' => __( 'Add size', 'optimole-wp' ),
2117 'add_image_size_desc' => __( 'Add New Image Crop Size', 'optimole-wp' ),
2118 'here' => __( ' here.', 'optimole-wp' ),
2119 'hide' => __( 'Hide', 'optimole-wp' ),
2120 'high_q_title' => __( 'High', 'optimole-wp' ),
2121 'image_1_label' => __( 'Original', 'optimole-wp' ),
2122 'image_2_label' => __( 'Optimized', 'optimole-wp' ),
2123 'lazyload_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Images load only when they\'re about to enter the viewport. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1948-bypass-lazy-load-for-first-images">', '</a>' ),
2124 'filter_length_error' => __( 'The filter should be at least 3 characters long.', 'optimole-wp' ),
2125 'scale_desc' => __( 'Automatically resize the images based on device and viewport', 'optimole-wp' ),
2126 'low_q_title' => __( 'Low', 'optimole-wp' ),
2127 'medium_q_title' => __( 'Medium', 'optimole-wp' ),
2128 'no_images_found' => __( 'You dont have any images in your Media Library. Add one and check how the Optimole will perform.', 'optimole-wp' ),
2129 'native_desc' => __( 'Uses the browser\'s built-in lazy loading feature. Enabling this will disable the auto scale.', 'optimole-wp' ),
2130 'option_saved' => __( 'Option saved.', 'optimole-wp' ),
2131 'ml_quality_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Optimole ML algorithms will predict the optimal image quality to get the smallest possible size with minimum perceived quality losses. When disabled, you can control the quality manually. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1016-what-is-the-difference-between-the-auto-high-medium-low-compression-levels">', '</a>' ),
2132 'quality_desc' => __( 'Lower image quality might boost your loading speed by lowering the size. However, the low image quality may negatively impact the visual appearance of the images. Try experimenting with the setting, then click the View sample image link to see what option works best for you.', 'optimole-wp' ),
2133 'quality_selected_value' => __( 'Selected value', 'optimole-wp' ),
2134 'quality_slider_desc' => __( 'See one sample image which will help you choose the right quality of the compression.', 'optimole-wp' ),
2135 'quality_title' => __( 'Auto Quality Powered by ML(Machine Learning)', 'optimole-wp' ),
2136 'strip_meta_title' => __( 'Strip Image Metadata', 'optimole-wp' ),
2137 'strip_meta_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Removes extra information from images, including EXIF and IPTC data (like camera settings and copyright info). This makes the pictures lighter and helps your website load faster. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1944-strip-image-metadata">', '</a>' ),
2138 'replacer_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'When enabled, Optimole will manage, optimize, and serve all the images on your website. If disabled, optimization, lazy loading, and other features will no longer be available. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1218-how-to-turn-off-image-replacement-in-optimoles-wordpress-plugin">', '</a>' ),
2139 'sample_image_loading' => __( 'Loading a sample image.', 'optimole-wp' ),
2140 'save_changes' => __( 'Save changes', 'optimole-wp' ),
2141 'show' => __( 'Show', 'optimole-wp' ),
2142 'selected_sites_title' => __( 'CURRENTLY SHOWING IMAGES FROM', 'optimole-wp' ),
2143 'selected_sites_desc' => __( 'Site:', 'optimole-wp' ),
2144 'selected_all_sites_desc' => __( 'Currently viewing images from all sites', 'optimole-wp' ),
2145 'select_all_sites_desc' => __( 'View images from all sites', 'optimole-wp' ),
2146 'select_site' => __( 'Select a website', 'optimole-wp' ),
2147 'cloud_site_title' => __( 'Show images only from these sites:', 'optimole-wp' ),
2148 'cloud_site_desc' => __( 'Browse images only from the specified websites. Otherwise, images from all websites will appear in the library.', 'optimole-wp' ),
2149 'toggle_ab_item' => __( 'Admin bar status', 'optimole-wp' ),
2150 'toggle_lazyload' => __( 'Enable Lazy Loading & Scaling', 'optimole-wp' ),
2151 'toggle_scale' => __( 'Smart Image Scaling', 'optimole-wp' ),
2152 'toggle_native' => __( 'Native Browser Loading', 'optimole-wp' ),
2153 'on_toggle' => __( 'On', 'optimole-wp' ),
2154 'off_toggle' => __( 'Off', 'optimole-wp' ),
2155 'view_sample_image' => __( 'View sample image', 'optimole-wp' ),
2156 'watch_placeholder_lazyload' => __( 'Optional selectors to extend coverage (one per line or comma-separated)', 'optimole-wp' ),
2157 // translators: %s is the example selector list.
2158 'watch_placeholder_lazyload_example' => sprintf( __( 'Example: %s', 'optimole-wp' ), '.hero-bg, #banner-image, .lazy-bg, .footer-bg' ),
2159 'watch_desc_lazyload' => __( 'If everything is covered out of the box, leave this empty.', 'optimole-wp' ),
2160 'smart_loading_title' => __( 'Smart Loading', 'optimole-wp' ),
2161 'smart_loading_desc' => __( 'JavaScript-driven with advanced controls', 'optimole-wp' ),
2162 'browser_native_lazy' => __( 'Uses the browser\'s built-in lazy loading feature', 'optimole-wp' ),
2163 // translators: viewport is the visible area of a web page on a display device.
2164 'viewport_detection' => __( 'Viewport detection', 'optimole-wp' ),
2165 // translators: set the colors for the placeholder image.
2166 'placeholders_color' => __( 'Placeholders', 'optimole-wp' ),
2167 // translators: it can handle many images without slowing down the site.
2168 'auto_scaling' => __( 'Auto Scaling', 'optimole-wp' ),
2169 // translators: it uses the browser's built-in lazy loading feature without any additional scripts.
2170 'lightweight_native' => __( 'Lightweight', 'optimole-wp' ),
2171
2172 'watch_title_lazyload' => __( 'Extend CSS Background Images', 'optimole-wp' ),
2173 'width_field' => __( 'Width', 'optimole-wp' ),
2174 'crop' => __( 'crop', 'optimole-wp' ),
2175 'toggle_cdn' => __( 'Serve CSS & JS Through Optimole', 'optimole-wp' ),
2176 'cdn_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'When enabled, Optimole will optimize your CSS and JS files and, if they contain images, the images as well, then deliver them via the CDN for faster webpage loading. %1$sLearn more%2$s', 'optimole-wp' ), '<a class="inline-block text-purple-gray underline" target=”_blank” href="https://docs.optimole.com/article/1966-serve-css-js-through-optimole">', '</a>' ),
2177 'enable_css_minify_title' => __( 'Minify CSS files', 'optimole-wp' ),
2178 'css_minify_desc' => __( 'Once Optimole will serve your CSS files, it will also minify the files and serve them via CDN.', 'optimole-wp' ),
2179 'enable_js_minify_title' => __( 'Minify JS files', 'optimole-wp' ),
2180 'js_minify_desc' => __( 'Once Optimole will serve your JS files, it will also minify the files and serve them via CDN.', 'optimole-wp' ),
2181 'sync_title' => __( 'Offload Existing Images', 'optimole-wp' ),
2182 'rollback_title' => __( 'Restore Offloaded Images', 'optimole-wp' ),
2183 'sync_desc' => __( 'Right now all the new images uploaded to your site are moved automatically to Optimole Cloud. In order to offload the existing ones, please click Sync images and wait for the process to finish. You can rollback anytime.', 'optimole-wp' ),
2184 'rollback_desc' => __( 'Pull all the offloaded images to Optimole Cloud back to your server.', 'optimole-wp' ),
2185 'sync_media' => __( 'Sync images', 'optimole-wp' ),
2186 'rollback_media' => __( 'Rollback images', 'optimole-wp' ),
2187 'sync_media_progress' => __( 'Moving your images to Optimole...', 'optimole-wp' ),
2188 'estimated_time' => __( 'Estimated time remaining', 'optimole-wp' ),
2189 'calculating_estimated_time' => __( 'We are currently calculating the estimated time for this job...', 'optimole-wp' ),
2190 'images_processing' => __( 'We are currently processing your images in the background. Leaving the page won\'t stop the process.', 'optimole-wp' ),
2191 'active_optimize_exclusions' => __( 'Active Optimizing Exclusions', 'optimole-wp' ),
2192 'active_lazyload_exclusions' => __( 'Active Lazy-loading Exclusions', 'optimole-wp' ),
2193 'minutes' => __( 'minutes', 'optimole-wp' ),
2194 'stop' => __( 'Stop', 'optimole-wp' ),
2195 'show_logs' => __( 'Show Logs', 'optimole-wp' ),
2196 'hide_logs' => __( 'Hide Logs', 'optimole-wp' ),
2197 'view_logs' => __( 'View Full Logs', 'optimole-wp' ),
2198 'rollback_media_progress' => __( 'Moving images into your media library...', 'optimole-wp' ),
2199 'rollback_media_error' => __( 'An unexpected error occured while pulling the offloaded back to your site', 'optimole-wp' ),
2200 'rollback_media_error_desc' => __( 'You can try again to pull back the rest of the images.', 'optimole-wp' ),
2201 'remove_notice' => __( 'Remove notice', 'optimole-wp' ),
2202 'sync_media_error' => __( 'An unexpected error occured while offloading all existing images from your site to Optimole', 'optimole-wp' ),
2203 'sync_media_link' => __( 'The selected images have been offloaded to our servers, you can check them', 'optimole-wp' ),
2204 'rollback_media_link' => __( 'The selected images have been restored to your server, you can check them', 'optimole-wp' ),
2205 'sync_media_error_desc' => __( 'You can try again to offload the rest of the images to Optimole.', 'optimole-wp' ),
2206 'offload_disable_warning_title' => __( 'Important! Please read carefully', 'optimole-wp' ),
2207 'offload_disable_warning_desc' => __( 'If you turn off this option, you will not be able to see the images in the Media Library without restoring the images first. Do you want to restore the images to your site upon turning off the option?', 'optimole-wp' ),
2208 'offload_enable_info_desc' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'You are not required to use the offload functionality for the plugin to work, use it if you want to save on hosting space. %1$s More details%2$s', 'optimole-wp' ), '<a style="white-space: nowrap;" target=”_blank” href="https://docs.optimole.com/article/1323-cloud-library-browsing">', '<span style="font-size:15px; margin-top:2px;" class="dashicons dashicons-external"></span></a>' ),
2209 'offload_conflicts_part_1' => __( 'We have detected the following plugins that conflict with the offload features:', 'optimole-wp' ),
2210 'offload_conflicts_part_2' => __( 'Please disable those plugins temporarily in order for Optimole to rollback the images to your site.', 'optimole-wp' ),
2211 'offloading_success' => sprintf( /* translators: 1 is the starting bold tag, 2 is the ending bold tag */ __( '%s Your images are now stored in Optimole Cloud.', 'optimole-wp' ), '<strong>' . __( 'Transfer Complete.', 'optimole-wp' ) . '</strong>' ),
2212 'rollback_success' => sprintf( /* translators: 1 is the starting bold tag, 2 is the ending bold tag */ __( '%s Your images have been restored to your website.', 'optimole-wp' ), '<strong>' . __( 'Transfer Complete.', 'optimole-wp' ) . '</strong>' ),
2213 'offloading_radio_legend' => __( 'Where your images are stored', 'optimole-wp' ),
2214 'offload_radio_option_rollback_title' => __( 'Optimole Cloud and your website', 'optimole-wp' ),
2215 'offload_radio_option_rollback_desc' => __( 'Images are stored in both the local WordPress media library and Optimole Cloud.', 'optimole-wp' ),
2216 'offload_radio_option_offload_title' => __( 'Optimole Cloud only', 'optimole-wp' ),
2217 'offload_radio_option_offload_desc' => __( 'Images are stored only in Optimole Cloud, allowing you to save space on your server. When enabled, any new images you upload in the Media Library will be automatically transferred to Optimole Cloud.', 'optimole-wp' ),
2218 'offload_limit_reached' => sprintf( /* translators: Current limit of offloaded images */ __( 'You have reached the maximum offloading limit of %s images. To increase the offload limit and for more information, contact our support.', 'optimole-wp' ), '<strong>#offload_limit#</strong>' ),
2219 'select' => __( 'Please select one ...', 'optimole-wp' ),
2220 'yes' => __( 'Restore images after disabling', 'optimole-wp' ),
2221 'no' => __( 'Do not restore images after disabling', 'optimole-wp' ),
2222 'lazyload_placeholder_color' => __( 'Placeholder Color', 'optimole-wp' ),
2223 'clear' => __( 'Clear', 'optimole-wp' ),
2224 'settings_saved' => __( 'Settings saved', 'optimole-wp' ),
2225 'settings_saved_error' => __( 'Error saving settings. Please reload the page and try again.', 'optimole-wp' ),
2226 'cache_cleared' => __( 'Cache cleared', 'optimole-wp' ),
2227 'cache_cleared_error' => __( 'Error clearing cache. Please reload the page and try again.', 'optimole-wp' ),
2228 'offloading_start_title' => __( 'Transfer your images to Optimole', 'optimole-wp' ),
2229 'offloading_start_description' => __( 'This process will transfer and store your images in Optimole Cloud and may take a while, depending on the number of images.', 'optimole-wp' ),
2230 'offloading_start_action' => __( 'Transfer to Optimole Cloud', 'optimole-wp' ),
2231 'offloading_stop_title' => __( 'Are you sure?', 'optimole-wp' ),
2232 'offloading_stop_description' => __( 'This will halt the ongoing process. To retrieve images transferred from the Optimole Cloud, use the Rollback option.', 'optimole-wp' ),
2233 'offloading_stop_action' => __( 'Cancel the transfer to Optimole', 'optimole-wp' ),
2234 'rollback_start_title' => __( 'Transfer back all images to your site', 'optimole-wp' ),
2235 'rollback_start_description' => __( 'This process will transfer back all images from Optimole to your website and may take a while, depending on the number of images.', 'optimole-wp' ),
2236 'rollback_start_action' => __( 'Transfer back from Optimole', 'optimole-wp' ),
2237 'rollback_stop_title' => __( 'Are you sure?', 'optimole-wp' ),
2238 'rollback_stop_description' => __( 'Canceling will halt the ongoing process, and any remaining images will stay in the Optimole Cloud. To transfer images to the Optimole Cloud, use the Offloading option.', 'optimole-wp' ),
2239 'rollback_stop_action' => __( 'Cancel the transfer from Optimole', 'optimole-wp' ),
2240 'cloud_library_btn_text' => __( 'Go to Cloud Library', 'optimole-wp' ),
2241 'cloud_library_btn_link' => add_query_arg( 'page', 'optimole-dam', admin_url( 'admin.php' ) ),
2242 'exceed_plan_quota_notice_title' => __( 'Your site has already reached over 50% of your monthly visits limit within just two weeks.', 'optimole-wp' ),
2243 'exceed_plan_quota_notice_description' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'Based on this trend, you are likely to exceed your free quota before the month ends. To avoid any disruption in service, we strongly recommend %1$supgrading%2$s your plan or waiting until your traffic stabilizes before offloading your images. Do you still wish to proceed?', 'optimole-wp' ), '<a style="white-space: nowrap;" target=”_blank” href="https://dashboard.optimole.com/settings/billing/">', '</a>' ),
2244 'exceed_plan_quota_notice_start_action' => __( 'Yes, Transfer to Optimole Cloud', 'optimole-wp' ),
2245 'exceed_plan_quota_notice_secondary_action' => __( 'No, keep images on my website', 'optimole-wp' ),
2246 'plan_update_notice_title' => __( 'Plan Update', 'optimole-wp' ),
2247 'plan_update_notice_desc' => __( 'We\'ve changed how plans work. Users on the Optimole Free plan cannot offload new images. Existing images that are already offloaded will remain offloaded.', 'optimole-wp' ),
2248 'upgrade_to_use_offloading_notice_desc' => __( 'Offloading images is a PRO feature. Please upgrade your plan to enable image transfer to Optimole Cloud.', 'optimole-wp' ),
2249 'visual_settings' => __( 'Visual Settings', 'optimole-wp' ),
2250 'extended_features' => __( 'Extended Features', 'optimole-wp' ),
2251 // translators: mark that the options are aplied globally.
2252 'global_option' => __( 'Global', 'optimole-wp' ),
2253 // translators: This option is discouraged from being used.
2254 'not_recommended' => __( 'Not recommended', 'optimole-wp' ),
2255 // translators: %1$s is the starting bold tag, %2$s is the ending bold tag.
2256 'viewport_skip_images_notice' => sprintf( __( 'When %1$sskip above the fold%2$s and %1$sskip first images%2$s are both enabled: the skip rule applies on a user\'s first page view. On subsequent views, once viewport data exists, the skip rule is ignored in favor of the viewport data.', 'optimole-wp' ), '<strong>', '</strong>' ),
2257 // translators: %1$s is the starting bold tag, %2$s is the ending bold tag.
2258 'native_lazy_load_warning' => sprintf( __( 'Native browser loading works with viewport detection, but it does not support smart image scaling. It can offer better cross-browser compatibility. Still, we do %1$snot recommend%2$s it for most sites due to fewer controls and features compared to Smart Loading.', 'optimole-wp' ), '<strong>', '</strong>' ),
2259 'performance_impact_alert_title' => __( 'Performance Impact Alert', 'optimole-wp' ),
2260 'performance_impact_alert_lazy_desc' => __( 'Disabling Lazy Load may significantly impact your site\'s loading speed and Core Web Vitals scores.', 'optimole-wp' ),
2261 'performance_impact_alert_scale_desc' => __( 'Disabling Image Scaling may significantly impact your site\'s loading speed and Core Web Vitals scores.', 'optimole-wp' ),
2262 'performance_impact_alert_action_label' => __( 'Continue with disabling', 'optimole-wp' ),
2263 'performance_impact_alert_secondary_action_label' => __( 'Keep enabled and get help', 'optimole-wp' ),
2264 ],
2265 'help' => [
2266 'section_one_title' => __( 'Help and Support', 'optimole-wp' ),
2267 'section_two_title' => __( 'Documentation', 'optimole-wp' ),
2268 'section_two_sub' => __( 'Docs Page', 'optimole-wp' ),
2269 'get_support_title' => __( 'Get Support', 'optimole-wp' ),
2270 'get_support_desc' => __( 'Need help or got a question? Submit a ticket and we\'ll get back to you.', 'optimole-wp' ),
2271 'get_support_cta' => __( 'Contact Support', 'optimole-wp' ),
2272 'feat_request_title' => __( 'Have a feature request?', 'optimole-wp' ),
2273 'feat_request_desc' => __( 'Help us improve Optimole by sharing feedback and ideas for new features.', 'optimole-wp' ),
2274 'feat_request_cta' => __( 'Submit a Feature Request', 'optimole-wp' ),
2275 'feedback_title' => __( 'Changelog', 'optimole-wp' ),
2276 'feedback_desc' => __( 'Check our changelog to see latest fixes and features implemented.', 'optimole-wp' ),
2277 'feedback_cta' => __( 'View Changelog', 'optimole-wp' ),
2278 'account_title' => __( 'Account', 'optimole-wp' ),
2279 'account_item_one' => __( 'How Optimole counts the visitors?', 'optimole-wp' ),
2280 'account_item_two' => __( 'What happens if I exceed plan limits?', 'optimole-wp' ),
2281 'account_item_three' => __( 'Visits based plan', 'optimole-wp' ),
2282 'image_processing_title' => __( 'Image Processing', 'optimole-wp' ),
2283 'image_processing_item_one' => __( 'Getting Started With Optimole', 'optimole-wp' ),
2284 'image_processing_item_two' => __( 'How Optimole can serve WebP images', 'optimole-wp' ),
2285 'image_processing_item_three' => __( 'Adding Watermarks to your images', 'optimole-wp' ),
2286 'api_title' => __( 'API', 'optimole-wp' ),
2287 'api_item_one' => __( 'Cloud Library Browsing', 'optimole-wp' ),
2288 'api_item_two' => __( 'Exclude from Optimizing or Lazy Loading', 'optimole-wp' ),
2289 'api_item_three' => __( 'Custom Integration', 'optimole-wp' ),
2290 ],
2291 'watermarks' => [
2292 'image' => __( 'Image', 'optimole-wp' ),
2293 'loading_remove_watermark' => __( 'Removing watermark resource ...', 'optimole-wp' ),
2294 'max_allowed' => __( 'You are allowed to save maximum 5 images.', 'optimole-wp' ),
2295 'list_header' => __( 'Possible watermarks', 'optimole-wp' ),
2296 'settings_header' => __( 'Watermarks position settings', 'optimole-wp' ),
2297 'no_images_found' => __( 'No images available for watermark. Please upload one.', 'optimole-wp' ),
2298 'id' => __( 'ID', 'optimole-wp' ),
2299 'name' => __( 'Name', 'optimole-wp' ),
2300 'type' => __( 'Type', 'optimole-wp' ),
2301 'action' => __( 'Action', 'optimole-wp' ),
2302 'upload' => __( 'Upload', 'optimole-wp' ),
2303 'add_desc' => __( 'Add new watermark', 'optimole-wp' ),
2304 'wm_title' => __( 'Active watermark', 'optimole-wp' ),
2305 'wm_desc' => __( 'The active watermark to use from the list of uploaded watermarks.', 'optimole-wp' ),
2306 'opacity_field' => __( 'Opacity', 'optimole-wp' ),
2307 'opacity_title' => __( 'Watermark opacity', 'optimole-wp' ),
2308 'opacity_desc' => __( 'A value between 0 and 100 for the opacity level. If set to 0 it will disable the watermark.', 'optimole-wp' ),
2309 'position_title' => __( 'Watermark position', 'optimole-wp' ),
2310 'position_desc' => __( 'The place relative to the image where the watermark should be placed.', 'optimole-wp' ),
2311 'pos_nowe_title' => __( 'North-West', 'optimole-wp' ),
2312 'pos_no_title' => __( 'North', 'optimole-wp' ),
2313 'pos_noea_title' => __( 'North-East', 'optimole-wp' ),
2314 'pos_we_title' => __( 'West', 'optimole-wp' ),
2315 'pos_ce_title' => __( 'Center', 'optimole-wp' ),
2316 'pos_ea_title' => __( 'East', 'optimole-wp' ),
2317 'pos_sowe_title' => __( 'South-West', 'optimole-wp' ),
2318 'pos_so_title' => __( 'South', 'optimole-wp' ),
2319 'pos_soea_title' => __( 'South-East', 'optimole-wp' ),
2320 'offset_x_field' => __( 'Offset X', 'optimole-wp' ),
2321 'offset_y_field' => __( 'Offset Y', 'optimole-wp' ),
2322 'offset_title' => __( 'Watermark offset', 'optimole-wp' ),
2323 'offset_desc' => __( 'Offset the watermark from set position on X and Y axis. Values can be positive or negative.', 'optimole-wp' ),
2324 'scale_field' => __( 'Scale', 'optimole-wp' ),
2325 'scale_title' => __( 'Watermark scale', 'optimole-wp' ),
2326 'scale_desc' => __( 'A value between 0 and 300 for the scale of the watermark (100 is the original size and 300 is 3x the size) relative to the resulting image size. If set to 0 it will default to the original size.', 'optimole-wp' ),
2327 'save_changes' => __( 'Save changes', 'optimole-wp' ),
2328 ],
2329 'latest_images' => [
2330 'image' => __( 'Image', 'optimole-wp' ),
2331 'no_images_found' => sprintf( /* translators: 1 is the starting anchor tag, 2 is the ending anchor tag */ __( 'We are currently optimizing your images. Meanwhile you can visit your %1$shomepage%2$s and check how our plugin performs.', 'optimole-wp' ), '<a href="' . esc_url( home_url() ) . '" target="_blank" >', '</a>' ),
2332 'compression' => __( 'Optimization', 'optimole-wp' ),
2333 'loading_latest_images' => __( 'Loading your optimized images...', 'optimole-wp' ),
2334 'last_optimized_images' => __( 'Last optimized images', 'optimole-wp' ),
2335 // translators: %s is the percentage (e.g. 10%).
2336 'percentage_saved' => sprintf( __( '%s Saved', 'optimole-wp' ), '{ratio}%' ),
2337 // translators: %s is the percentage (e.g. 10%).
2338 'percentage_smaller' => sprintf( __( '%s smaller', 'optimole-wp' ), '{ratio}%' ),
2339 'same_size' => __( '🙉 We couldn\'t do better, this image is already optimized at maximum.', 'optimole-wp' ),
2340 'small_optimization' => __( '😬 Not that much, just <strong>{ratio}</strong> smaller.', 'optimole-wp' ),
2341 'medium_optimization' => __( '🤓 We are on the right track, <strong>{ratio}</strong> squeezed.', 'optimole-wp' ),
2342 'big_optimization' => __( '❤️❤️❤️ Our moles just nailed it, this one is <strong>{ratio}</strong> smaller.', 'optimole-wp' ),
2343 ],
2344 'csat' => [
2345 'close' => __( 'Close', 'optimole-wp' ),
2346 ],
2347 'cron_error' => sprintf( /* translators: 1 is code to disable cron, 2 value of the constant */ __( 'It seems that you have the %1$s constant defined as %2$s. The offloading process uses cron events to offload the images in the background. Please remove the constant from your wp-config.php file in order for the offloading process to work.', 'optimole-wp' ), '<code>DISABLE_WP_CRON</code>', '<code>true</code>' ),
2348 'cancel' => __( 'Cancel', 'optimole-wp' ),
2349 'optimization_status' => [
2350 'title' => __( 'Optimization Status', 'optimole-wp' ),
2351 'statusTitle1' => __( 'Image Handling', 'optimole-wp' ),
2352 'statusSubTitle1' => __( 'All images are optimized automatically', 'optimole-wp' ),
2353 'statusTitle2' => __( 'Smart Lazy-Loading', 'optimole-wp' ),
2354 'statusSubTitle2' => __( 'Images load as visitors scroll', 'optimole-wp' ),
2355 'statusTitle3' => __( 'Image Scalling', 'optimole-wp' ),
2356 'statusSubTitle3' => __( 'All images are perfectly sized for devices', 'optimole-wp' ),
2357 'disable' => __( 'Disable', 'optimole-wp' ),
2358 'enable' => __( 'Enable', 'optimole-wp' ),
2359 'manage' => __( 'Manage', 'optimole-wp' ),
2360 ],
2361 'optimization_tips' => sprintf(
2362 /* translators: 1 is the opening anchor tag, 2 is the closing anchor tag */
2363 __( '%1$sView all optimization tips%2$s', 'optimole-wp' ),
2364 '<a class="flex justify-center items-center font-semibold" href="https://docs.optimole.com/article/2238-optimization-tips" target="_blank"> ',
2365 '<span style="text-decoration:none; font-size:15px; margin-top:2px;" class="dashicons dashicons-external"></span></a>'
2366 ),
2367 'contact_support' => [
2368 // translators: %s is the email main subject.
2369 'title_prefix' => __( '[Lazy Load Issue] %s', 'optimole-wp' ),
2370 'disable_lazy_load_scaling' => __( 'Disable Lazy Load & Scaling', 'optimole-wp' ),
2371 'disable_image_scaling' => __( 'Disable Image Scaling', 'optimole-wp' ),
2372 'enable_native_lazy_load' => __( 'Enable Native Lazy Load', 'optimole-wp' ),
2373 ],
2374 // translators: %s is the date of the renewal.
2375 'renew_date' => __( 'Renews %s', 'optimole-wp' ),
2376 ];
2377 }
2378
2379 /**
2380 * Allow SVG uploads
2381 *
2382 * @param array $mimes Supported mimes.
2383 *
2384 * @return array
2385 * @access public
2386 * @uses filter:upload_mimes
2387 */
2388 public function allow_svg( $mimes ) {
2389 $mimes['svg'] = 'image/svg+xml';
2390
2391 return $mimes;
2392 }
2393
2394 /**
2395 * Get the Formbricks survey metadata.
2396 *
2397 * @param array $data The data in Formbricks format.
2398 * @param string $page_slug The slug of the page.
2399 *
2400 * @return array - The data in Formbricks format.
2401 */
2402 public function get_survey_metadata( $data, $page_slug ) {
2403
2404 if ( 'dashboard' !== $page_slug ) {
2405 return $data;
2406 }
2407
2408 $dashboard_data = $this->localize_dashboard_app();
2409 $user_data = $dashboard_data['user_data'];
2410
2411 if ( ! isset( $user_data['plan'] ) ) {
2412 return $data;
2413 }
2414
2415 $data = [
2416 'environmentId' => 'clo8wxwzj44orpm0gjchurujm',
2417 'attributes' => [
2418 'plan' => $user_data['plan'],
2419 'status' => $user_data['status'],
2420 'cname_assigned' => ! empty( $user_data['is_cname_assigned'] ) ? $user_data['is_cname_assigned'] : 'no',
2421 'connected_websites' => isset( $user_data['whitelist'] ) ? strval( count( $user_data['whitelist'] ) ) : '0',
2422 'install_days_number' => intval( $dashboard_data['days_since_install'] ),
2423 'traffic' => strval( isset( $user_data['traffic'] ) ? $this->survey_category( $user_data['traffic'], 500 ) : 0 ),
2424 'images_number' => strval( isset( $user_data['images_number'] ) ? $this->survey_category( $user_data['images_number'], 100 ) : 0 ),
2425 ],
2426 ];
2427
2428 return $data;
2429 }
2430
2431 /**
2432 * Categorize a number for survey based on its scale.
2433 *
2434 * @param int $value The value.
2435 * @param int $scale The scale.
2436 *
2437 * @return int - The category.
2438 */
2439 public function survey_category( $value, $scale = 1 ) {
2440 $value = intval( $value / $scale );
2441
2442 if ( 1 < $value && 8 > $value ) {
2443 return 7;
2444 }
2445
2446 if ( 8 <= $value && 31 > $value ) {
2447 return 30;
2448 }
2449
2450 if ( 30 < $value && 90 > $value ) {
2451 return 90;
2452 }
2453
2454 if ( 90 <= $value ) {
2455 return 91;
2456 }
2457
2458 return 0;
2459 }
2460
2461 /**
2462 * Determines whether the exceed quota warning should be displayed to users.
2463 *
2464 * This function checks if the user's quota usage has exceeded a predefined limit
2465 * and returns a boolean value indicating whether the warning should be shown.
2466 *
2467 * @return bool True if the exceed quota warning should be displayed, false otherwise.
2468 */
2469 public function should_show_exceed_quota_warning() {
2470 if ( ! $this->settings->is_connected() ) {
2471 return false;
2472 }
2473 if ( get_option( 'optml_notice_hide_upg', 'no' ) === 'yes' ) {
2474 return false;
2475 }
2476
2477 $service_data = $this->settings->get( 'service_data' );
2478
2479 if ( ! isset( $service_data['plan'] ) ) {
2480 return false;
2481 }
2482 if ( $service_data['plan'] !== 'free' ) {
2483 return false;
2484 }
2485 if ( ! isset( $service_data['renews_on'] ) ) {
2486 return false;
2487 }
2488 $renews_on = $service_data['renews_on'];
2489 $timestamp_before_two_weeks = strtotime( '-2 weeks', $renews_on );
2490 $today_timestamp = strtotime( 'today' );
2491
2492 if ( $timestamp_before_two_weeks < $today_timestamp ) {
2493 return false;
2494 }
2495
2496 $visitors_limit = isset( $service_data['visitors_limit'] ) ? (int) $service_data['visitors_limit'] : 0;
2497 $visitors_left = isset( $service_data['visitors_left'] ) ? (int) $service_data['visitors_left'] : 0;
2498
2499 if ( ! $visitors_limit || ! $visitors_left ) {
2500 return false;
2501 }
2502
2503 $used_quota = $visitors_limit - $visitors_left;
2504 $is_50_percent_used = ( $used_quota / $visitors_limit ) >= 0.5;
2505
2506 if ( ! $is_50_percent_used ) {
2507 return false;
2508 }
2509
2510 return true;
2511 }
2512
2513 /**
2514 * Get the number of active notices (not dismissed).
2515 *
2516 * @return int - Number of active notices
2517 */
2518 private function get_active_notices_count() {
2519 $conflicting_plugins = $this->conflicting_plugins->get_conflicting_plugins();
2520 $conflicts_count = 0;
2521
2522 foreach ( $conflicting_plugins as $key => $plugin ) {
2523 $key = str_replace( 'wp-', '', $key );
2524 $class_name = 'Optml_' . ucfirst( $key );
2525
2526 if ( ! class_exists( $class_name ) ) {
2527 continue;
2528 }
2529 $conflict_instance = new $class_name();
2530
2531 if ( ! is_a( $conflict_instance, 'Optml_Abstract_Conflict' ) ) {
2532 continue;
2533 }
2534
2535 if ( $conflict_instance->is_conflict_valid() ) {
2536 ++$conflicts_count;
2537 }
2538 }
2539
2540 $dismissed_notices = get_option( 'optml_dismissed_conflicts', [] );
2541
2542 return $conflicts_count - count( $dismissed_notices );
2543 }
2544
2545 /**
2546 * Mark if the user had offloading enabled on first run.
2547 *
2548 * If it is an old free user that had offloading enabled, we will use the mark to show a notice about the plan changes.
2549 *
2550 * @return void
2551 */
2552 public function mark_user_with_offload() {
2553 if ( ! $this->settings->is_connected() ) {
2554 return;
2555 }
2556
2557 if ( false !== get_option( 'optml_has_offloading_enabled_on_upgrade', false ) ) {
2558 return;
2559 }
2560
2561 update_option( 'optml_has_offloading_enabled_on_upgrade', $this->settings->is_offload_enabled() ? 'yes' : 'no' );
2562 }
2563 }
2564