PluginProbe
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization / 4.2.16
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization v4.2.16
4.2.16 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 All 137 releases
optimole-wp / inc / admin.php

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

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