PluginProbe
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization / trunk
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization vtrunk
4.2.14 4.2.13 4.2.12 4.2.11 4.2.10 4.2.9 4.2.8 4.2.7 4.2.6 4.2.5 2.5.5 2.5.6 2.5.7 3.0.0 3.0.1 3.1.0 3.1.1 3.1.2 3.1.3 3.10.0 3.11.0 3.11.1 3.11.2 3.11.3 3.12.0 All 135 releases
optimole-wp / vendor / codeinwp / themeisle-sdk / src / Modules / Promotions.php

Promotions.php in Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization trunk, at vendor/codeinwp/themeisle-sdk/src/Modules/Promotions.php

2,081 lines 78.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * The promotions model class for ThemeIsle SDK
4 *
5 * Here's how to hook it in your plugin: add_filter( 'menu_icons_load_promotions', function() { return array( 'otter' ); } );
6 *
7 * @package ThemeIsleSDK
8 * @subpackage Modules
9 * @copyright Copyright (c) 2017, Marius Cristea
10 * @license http://opensource.org/licenses/gpl-3.0.php GNU Public License
11 * @since 1.0.0
12 */
13
14 namespace ThemeisleSDK\Modules;
15
16 use ThemeisleSDK\Common\Abstract_Module;
17 use ThemeisleSDK\Loader;
18 use ThemeisleSDK\Product;
19
20 // Exit if accessed directly.
21 if ( ! defined( 'ABSPATH' ) ) {
22 exit;
23 }
24
25 /**
26 * Promotions module for ThemeIsle SDK.
27 */
28 class Promotions extends Abstract_Module {
29
30 /**
31 * Holds the promotions.
32 *
33 * @var array
34 */
35 public $promotions = array();
36
37 /**
38 * Holds the values of the promotions that are not allowed to be shown.
39 * Can be filtered by each product.
40 *
41 * @var array
42 */
43 private $dissallowed_promotions = array();
44
45 /**
46 * Option key for promos.
47 *
48 * @var string
49 */
50 private $option_main = 'themeisle_sdk_promotions';
51
52 /**
53 * Option key for otter promos.
54 *
55 * @var string
56 */
57 private $option_otter = 'themeisle_sdk_promotions_otter_installed';
58
59 /**
60 * Option key for optimole promos.
61 *
62 * @var string
63 */
64 private $option_optimole = 'themeisle_sdk_promotions_optimole_installed';
65
66 /**
67 * Option key for ROP promos.
68 *
69 * @var string
70 */
71 private $option_rop = 'themeisle_sdk_promotions_rop_installed';
72
73 /**
74 * Option key for Neve promo.
75 *
76 * @var string
77 */
78 private $option_neve = 'themeisle_sdk_promotions_neve_installed';
79
80 /**
81 * Option key for Redirection for CF7.
82 *
83 * @var string
84 */
85 private $option_redirection_cf7 = 'themeisle_sdk_promotions_redirection_cf7_installed';
86
87 /**
88 * Option key for Hyve.
89 *
90 * @var string
91 */
92 private $option_hyve = 'themeisle_sdk_promotions_hyve_installed';
93
94 /**
95 * Option key for WP Full Pay.
96 *
97 * @var string
98 */
99 private $option_wp_full_pay = 'themeisle_sdk_promotions_wp_full_pay_installed';
100
101 /**
102 * Option key for Feedzy.
103 *
104 * @var string
105 */
106 private $option_feedzy = 'themeisle_sdk_promotions_feedzy_installed';
107
108 /**
109 * Option key for Masteriyo promos.
110 *
111 * @var string
112 */
113 private $option_masteriyo = 'themeisle_sdk_promotions_masteriyo_installed';
114
115 /**
116 * Option key for Easy MCP AI promos.
117 *
118 * @var string
119 */
120 private $option_easy_mcp = 'themeisle_sdk_promotions_easy_mcp_installed';
121
122 /**
123 * Loaded promotion.
124 *
125 * @var string
126 */
127 private $loaded_promo;
128
129 /**
130 * Whether promotions were processed for the current screen.
131 *
132 * @var bool
133 */
134 private $available_promotions_loaded = false;
135
136 /**
137 * Woo promotions.
138 *
139 * @var array
140 */
141 private $woo_promos = array();
142
143 /**
144 * Debug mode.
145 *
146 * @var bool
147 */
148 private $debug = false;
149
150 /**
151 * Should we load this module.
152 *
153 * @param Product $product Product object.
154 *
155 * @return bool
156 */
157 public function can_load( $product ) {
158 if ( apply_filters( 'themeisle_sdk_ran_promos', false ) === true ) {
159 return false;
160 }
161 if ( $this->is_from_partner( $product ) ) {
162 return false;
163 }
164
165 $this->debug = apply_filters( 'themeisle_sdk_promo_debug', $this->debug );
166 $promotions_to_load = apply_filters( $product->get_key() . '_load_promotions', array() );
167
168 $promotions_to_load[] = 'optimole';
169 $promotions_to_load[] = 'rop';
170 $promotions_to_load[] = 'woo_plugins';
171 $promotions_to_load[] = 'neve';
172 $promotions_to_load[] = 'redirection-cf7';
173 $promotions_to_load[] = 'hyve';
174 $promotions_to_load[] = 'wp_full_pay';
175 $promotions_to_load[] = 'feedzy_import';
176 $promotions_to_load[] = 'learning-management-system';
177 $promotions_to_load[] = 'easy-mcp';
178
179 if ( defined( 'NEVE_VERSION' ) || defined( 'WPMM_PATH' ) || defined( 'OTTER_BLOCKS_VERSION' ) || defined( 'OBFX_URL' ) ) {
180 $promotions_to_load[] = 'feedzy_embed';
181 }
182 $promotions_to_load = array_unique( $promotions_to_load );
183
184 $this->promotions = $this->get_promotions();
185
186 $this->dissallowed_promotions = apply_filters( $product->get_key() . '_dissallowed_promotions', array() );
187
188 foreach ( $this->promotions as $slug => $data ) {
189 if ( ! in_array( $slug, $promotions_to_load, true ) ) {
190 unset( $this->promotions[ $slug ] );
191 }
192 }
193 add_action( 'init', array( $this, 'register_settings' ), 99 );
194 add_action( 'admin_init', array( $this, 'register_reference' ), 99 );
195
196 return ! empty( $this->promotions );
197 }
198
199 /**
200 * Registers the hooks.
201 *
202 * @param Product $product Product to load.
203 */
204 public function load( $product ) {
205 if ( ! $this->is_writeable() || ! current_user_can( 'install_plugins' ) ) {
206 return;
207 }
208
209 $last_dismiss_time = $this->get_last_dismiss_time();
210
211 if ( ! $this->debug && is_int( $last_dismiss_time ) && ( time() - $last_dismiss_time ) < ( 3 * WEEK_IN_SECONDS ) ) {
212 return;
213 }
214
215 $this->product = $product;
216
217 add_filter( 'attachment_fields_to_edit', array( $this, 'add_attachment_field' ), 10, 2 );
218 add_action( 'current_screen', [ $this, 'load_available' ] );
219 add_action( 'elementor/editor/after_enqueue_scripts', array( $this, 'enqueue' ) );
220 add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_visualizer_block_editor_shim' ), 100 );
221 add_action( 'wp_ajax_tisdk_update_option', array( $this, 'dismiss_promotion' ) );
222 add_filter( 'plugins_api_result', array( $this, 'inject_visualizer_block_directory_suggestion' ), 10, 3 );
223 add_filter( 'option_visualizer-activated', array( $this, 'suppress_visualizer_onboarding_in_editor' ) );
224 add_filter( 'themeisle_sdk_ran_promos', '__return_true' );
225
226 if ( get_option( $this->option_neve, false ) !== true ) {
227 add_action( 'wp_ajax_themeisle_sdk_dismiss_notice', 'ThemeisleSDK\Modules\Notification::dismiss' );
228 }
229 }
230
231
232 /**
233 * Inject Visualizer as the first block-directory suggestion for chart queries.
234 *
235 * @param object|WP_Error $res Response object or WP_Error.
236 * @param string $action The API action.
237 * @param object $args The API arguments.
238 * @return object|WP_Error
239 */
240 public function inject_visualizer_block_directory_suggestion( $res, $action, $args ) {
241 if ( 'query_plugins' !== $action || ! is_object( $args ) || empty( $args->block ) ) {
242 return $res;
243 }
244
245 if ( ! $this->should_suggest_visualizer( $args->block ) ) {
246 return $res;
247 }
248
249 if ( $this->is_plugin_installed( 'visualizer' ) ) {
250 return $res;
251 }
252
253 $plugin = $this->get_visualizer_block_directory_data();
254 if ( empty( $plugin ) ) {
255 return $res;
256 }
257
258 if ( is_wp_error( $res ) ) {
259 $res = (object) array( 'plugins' => array() );
260 }
261
262 if ( ! isset( $res->plugins ) || ! is_array( $res->plugins ) ) {
263 $res->plugins = array();
264 }
265
266 $res->plugins = array_values(
267 array_filter(
268 $res->plugins,
269 function ( $existing ) use ( $plugin ) {
270 return ! isset( $existing['slug'] ) || $existing['slug'] !== $plugin['slug'];
271 }
272 )
273 );
274
275 array_unshift( $res->plugins, $plugin );
276
277 return $res;
278 }
279
280 /**
281 * Check if the query should trigger the Visualizer suggestion.
282 *
283 * @param string $term Search term.
284 * @return bool
285 */
286 private function should_suggest_visualizer( $term ) {
287 $term = strtolower( (string) $term );
288 return false !== strpos( $term, 'chart' ) || false !== strpos( $term, 'visualizer' ) || false !== strpos( $term, 'visualization' ) || false !== strpos( $term, 'graph' );
289 }
290
291 /**
292 * Build the plugin data for Visualizer block directory results.
293 *
294 * @return array
295 */
296 private function get_visualizer_block_directory_data() {
297 $slug = 'visualizer';
298 $plugin_info = $this->call_plugin_api( $slug );
299
300 if ( is_wp_error( $plugin_info ) || empty( $plugin_info ) ) {
301 return array();
302 }
303
304 $icons = array();
305 if ( ! empty( $plugin_info->icons ) ) {
306 if ( ! empty( $plugin_info->icons['1x'] ) ) {
307 $icons['1x'] = $plugin_info->icons['1x'];
308 }
309 if ( ! empty( $plugin_info->icons['2x'] ) ) {
310 $icons['2x'] = $plugin_info->icons['2x'];
311 }
312 }
313
314 $name = isset( $plugin_info->name ) ? $plugin_info->name : 'Visualizer';
315
316 return array(
317 'slug' => $slug,
318 'name' => $name,
319 'short_description' => isset( $plugin_info->short_description ) ? $plugin_info->short_description : '',
320 'author' => isset( $plugin_info->author ) ? wp_strip_all_tags( $plugin_info->author ) : '',
321 'rating' => isset( $plugin_info->rating ) ? (int) $plugin_info->rating : 0,
322 'num_ratings' => isset( $plugin_info->num_ratings ) ? (int) $plugin_info->num_ratings : 0,
323 'active_installs' => isset( $plugin_info->active_installs ) ? (int) $plugin_info->active_installs : 0,
324 'author_block_rating' => isset( $plugin_info->author_block_rating ) ? (int) $plugin_info->author_block_rating : 0,
325 'author_block_count' => isset( $plugin_info->author_block_count ) ? (int) $plugin_info->author_block_count : 0,
326 'icons' => $icons,
327 'last_updated' => isset( $plugin_info->last_updated ) ? $plugin_info->last_updated : gmdate( 'Y-m-d H:i:s' ),
328 'blocks' => array(
329 array(
330 'name' => 'visualizer/chart',
331 'title' => $name,
332 ),
333 ),
334 );
335 }
336
337
338 /**
339 * Prevent Visualizer onboarding redirects while in the block editor.
340 *
341 * @param mixed $value Option value.
342 * @return mixed
343 */
344 public function suppress_visualizer_onboarding_in_editor( $value ) {
345 if ( ! $this->is_block_editor_screen() ) {
346 return $value;
347 }
348
349 return false;
350 }
351
352 /**
353 * Add a small compatibility shim for Visualizer's block editor bundle.
354 *
355 * Visualizer's "Display an existing chart" flow reads
356 * `google.visualization.Version` before the Google Charts loader has fully
357 * populated `google.visualization`, which can throw after dynamic install.
358 *
359 * @return void
360 */
361 public function enqueue_visualizer_block_editor_shim() {
362 global $themeisle_sdk_max_version;
363
364 if ( ! $this->is_block_editor_screen() ) {
365 return;
366 }
367
368 if ( ! wp_script_is( 'visualizer-gutenberg-block', 'enqueued' ) ) {
369 return;
370 }
371
372 wp_register_script(
373 'ti-sdk-visualizer-editor-shim',
374 '',
375 array(),
376 $themeisle_sdk_max_version,
377 true
378 );
379 wp_enqueue_script( 'ti-sdk-visualizer-editor-shim' );
380 wp_add_inline_script(
381 'ti-sdk-visualizer-editor-shim',
382 'window.google = window.google || {}; window.google.visualization = window.google.visualization || {}; window.google.visualization.Version = window.google.visualization.Version || "current";'
383 );
384 }
385
386 /**
387 * Check if the current admin screen is the block editor.
388 *
389 * @return bool
390 */
391 private function is_block_editor_screen() {
392 if ( ! function_exists( 'get_current_screen' ) ) {
393 return $this->is_block_editor_request();
394 }
395
396 $screen = get_current_screen();
397 if ( is_object( $screen ) && method_exists( $screen, 'is_block_editor' ) && $screen->is_block_editor() ) {
398 return true;
399 }
400
401 return $this->is_block_editor_request();
402 }
403
404 /**
405 * Detect block editor requests before the current screen is available.
406 *
407 * @return bool
408 */
409 private function is_block_editor_request() {
410 global $pagenow;
411
412 if ( 'post-new.php' === $pagenow ) {
413 $post_type = isset( $_GET['post_type'] ) ? sanitize_key( $_GET['post_type'] ) : 'post';
414
415 return function_exists( 'use_block_editor_for_post_type' ) ? use_block_editor_for_post_type( $post_type ) : true;
416 }
417
418 if ( 'post.php' === $pagenow && isset( $_GET['post'] ) ) {
419 $post_id = absint( $_GET['post'] );
420
421 return function_exists( 'use_block_editor_for_post' ) ? use_block_editor_for_post( $post_id ) : true;
422 }
423
424 return false;
425 }
426
427 /**
428 * Load available promotions.
429 */
430 public function load_available() {
431 if ( $this->available_promotions_loaded ) {
432 return;
433 }
434
435 $this->available_promotions_loaded = true;
436 $this->promotions = $this->filter_by_screen_and_merge();
437 if ( empty( $this->promotions ) ) {
438 return;
439 }
440
441 $this->load_promotion( $this->promotions[ array_rand( $this->promotions ) ] );
442 }
443
444
445 /**
446 * Register plugin reference.
447 *
448 * @return void
449 */
450 public function register_reference() {
451 if ( ! current_user_can( 'activate_plugins' ) ) {
452 return;
453 }
454
455 if ( ! isset( $_GET['plugin'] ) || ! isset( $_GET['_wpnonce'] ) ) {
456 return;
457 }
458
459 $plugin = rawurldecode( $_GET['plugin'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
460 if ( wp_verify_nonce( $_GET['_wpnonce'], 'activate-plugin_' . $plugin ) === false ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
461 return;
462 }
463
464 if ( isset( $_GET['reference_key'] ) ) {
465 update_option( 'otter_reference_key', sanitize_key( $_GET['reference_key'] ) );
466 }
467
468 if ( isset( $_GET['optimole_reference_key'] ) ) {
469 update_option( 'optimole_reference_key', sanitize_key( $_GET['optimole_reference_key'] ) );
470 }
471
472 if ( isset( $_GET['rop_reference_key'] ) ) {
473 update_option( 'rop_reference_key', sanitize_key( $_GET['rop_reference_key'] ) );
474 }
475
476 if ( isset( $_GET['neve_reference_key'] ) ) {
477 update_option( 'neve_reference_key', sanitize_key( $_GET['neve_reference_key'] ) );
478 }
479
480 if ( isset( $_GET['hyve_reference_key'] ) ) {
481 update_option( 'hyve_reference_key', sanitize_key( $_GET['hyve_reference_key'] ) );
482 }
483
484 if ( isset( $_GET['wp_full_pay_reference_key'] ) ) {
485 update_option( 'wp_full_pay_reference_key', sanitize_key( $_GET['wp_full_pay_reference_key'] ) );
486 }
487
488 if ( isset( $_GET['easy_mcp_reference_key'] ) ) {
489 update_option( 'easy_mcp_reference_key', sanitize_key( $_GET['easy_mcp_reference_key'] ) );
490 }
491
492 if ( isset( $_GET['feedzy_reference_key'] ) || ( isset( $_GET['from'], $_GET['plugin'] ) && $_GET['from'] === 'import' && str_starts_with( sanitize_key( $_GET['plugin'] ), 'feedzy' ) ) ) {
493 update_option( 'feedzy_reference_key', sanitize_key( $_GET['feedzy_reference_key'] ?? 'i-' . $this->product->get_key() ) );
494 update_option( $this->option_feedzy, 1 );
495 }
496 }
497
498 /**
499 * Register Settings
500 */
501 public function register_settings() {
502 $default = get_option( 'themeisle_sdk_promotions_otter', '{}' );
503
504 register_setting(
505 'themeisle_sdk_settings',
506 $this->option_main,
507 array(
508 'type' => 'string',
509 'sanitize_callback' => 'sanitize_text_field',
510 'show_in_rest' => true,
511 'default' => $default,
512 )
513 );
514
515 register_setting(
516 'themeisle_sdk_settings',
517 $this->option_otter,
518 array(
519 'type' => 'boolean',
520 'sanitize_callback' => 'rest_sanitize_boolean',
521 'show_in_rest' => true,
522 'default' => false,
523 )
524 );
525 register_setting(
526 'themeisle_sdk_settings',
527 $this->option_optimole,
528 array(
529 'type' => 'boolean',
530 'sanitize_callback' => 'rest_sanitize_boolean',
531 'show_in_rest' => true,
532 'default' => false,
533 )
534 );
535 register_setting(
536 'themeisle_sdk_settings',
537 $this->option_rop,
538 array(
539 'type' => 'boolean',
540 'sanitize_callback' => 'rest_sanitize_boolean',
541 'show_in_rest' => true,
542 'default' => false,
543 )
544 );
545 register_setting(
546 'themeisle_sdk_settings',
547 $this->option_neve,
548 array(
549 'type' => 'boolean',
550 'sanitize_callback' => 'rest_sanitize_boolean',
551 'show_in_rest' => true,
552 'default' => false,
553 )
554 );
555 register_setting(
556 'themeisle_sdk_settings',
557 $this->option_redirection_cf7,
558 array(
559 'type' => 'boolean',
560 'sanitize_callback' => 'rest_sanitize_boolean',
561 'show_in_rest' => true,
562 'default' => false,
563 )
564 );
565 register_setting(
566 'themeisle_sdk_settings',
567 $this->option_hyve,
568 array(
569 'type' => 'boolean',
570 'sanitize_callback' => 'rest_sanitize_boolean',
571 'show_in_rest' => true,
572 'default' => false,
573 )
574 );
575 register_setting(
576 'themeisle_sdk_settings',
577 $this->option_wp_full_pay,
578 array(
579 'type' => 'boolean',
580 'sanitize_callback' => 'rest_sanitize_boolean',
581 'show_in_rest' => true,
582 'default' => false,
583 )
584 );
585 register_setting(
586 'themeisle_sdk_settings',
587 $this->option_masteriyo,
588 array(
589 'type' => 'boolean',
590 'sanitize_callback' => 'rest_sanitize_boolean',
591 'show_in_rest' => true,
592 'default' => false,
593 )
594 );
595 register_setting(
596 'themeisle_sdk_settings',
597 $this->option_easy_mcp,
598 array(
599 'type' => 'boolean',
600 'sanitize_callback' => 'rest_sanitize_boolean',
601 'show_in_rest' => true,
602 'default' => false,
603 )
604 );
605 }
606
607 /**
608 * Check if the path is writable.
609 *
610 * @return boolean
611 * @access public
612 */
613 public function is_writeable() {
614
615 include_once ABSPATH . 'wp-admin/includes/file.php';
616 $filesystem_method = get_filesystem_method();
617
618 if ( 'direct' === $filesystem_method ) {
619 return true;
620 }
621
622 return false;
623 }
624
625 /**
626 * Third-party compatibility.
627 *
628 * @return boolean
629 */
630 private function has_conflicts() {
631 global $pagenow;
632
633 // Editor notices aren't compatible with Enfold theme.
634 if ( defined( 'AV_FRAMEWORK_VERSION' ) && in_array( $pagenow, array( 'post.php', 'post-new.php' ) ) ) {
635 return true;
636 }
637
638 return false;
639 }
640
641 /**
642 * Get promotions.
643 *
644 * @return array
645 */
646 private function get_promotions() {
647 $has_otter = defined( 'OTTER_BLOCKS_VERSION' ) || $this->is_plugin_installed( 'otter-blocks' );
648 $had_otter_from_promo = get_option( $this->option_otter, false );
649 $has_optimole = defined( 'OPTIMOLE_VERSION' ) || $this->is_plugin_installed( 'optimole-wp' );
650 $had_optimole_from_promo = get_option( $this->option_optimole, false );
651 $has_rop = defined( 'ROP_LITE_VERSION' ) || $this->is_plugin_installed( 'tweet-old-post' );
652 $had_rop_from_promo = get_option( $this->option_rop, false );
653 $has_woocommerce = class_exists( 'WooCommerce' );
654 $has_sparks = defined( 'SPARKS_WC_VERSION' ) || $this->is_plugin_installed( 'sparks-for-woocommerce' );
655 $has_ppom = defined( 'PPOM_VERSION' ) || $this->is_plugin_installed( 'woocommerce-product-addon' );
656 $has_redirection_cf7 = defined( 'WPCF7_PRO_REDIRECT_PLUGIN_VERSION' ) || $this->is_plugin_installed( 'wpcf7-redirect' );
657 $had_redirection_cf7_promo = get_option( $this->option_redirection_cf7, false );
658 $is_min_php_7_4 = version_compare( PHP_VERSION, '7.4', '>=' );
659 $is_min_php_7_2 = version_compare( PHP_VERSION, '7.2', '>=' );
660 $can_check_plugin_install = $this->can_check_plugin_install_promo();
661 $has_hyve = defined( 'HYVE_LITE_VERSION' ) || $this->is_plugin_installed( 'hyve' ) || $this->is_plugin_installed( 'hyve-lite' );
662 $had_hyve_from_promo = get_option( $this->option_hyve, false );
663 $has_hyve_conditions = $is_min_php_7_4 && ! $has_hyve && ! $had_hyve_from_promo && version_compare( get_bloginfo( 'version' ), '6.2', '>=' ) && $can_check_plugin_install && $this->has_support_page();
664 $has_wfp_full_pay = defined( 'WP_FULL_STRIPE_BASENAME' ) || $this->is_plugin_installed( 'wp-full-stripe-free' );
665 $had_wfp_from_promo = get_option( $this->option_wp_full_pay, false );
666 $has_wfp_conditions = ! $has_wfp_full_pay && ! $had_wfp_from_promo && $can_check_plugin_install && $this->has_donate_page();
667 $is_min_req_v = version_compare( get_bloginfo( 'version' ), '5.8', '>=' );
668 $current_theme = wp_get_theme();
669 $has_neve = $current_theme->template === 'neve' || $current_theme->parent() === 'neve';
670 $has_neve_from_promo = get_option( $this->option_neve, false );
671 $has_enough_attachments = $this->has_min_media_attachments();
672 $has_enough_old_posts = $this->has_old_posts();
673 $has_feedzy = defined( 'FEEDZY_BASEFILE' ) || $this->is_plugin_installed( 'feedzy-rss-feedss' );
674 $had_feedzy_from_promo = get_option( $this->option_feedzy, false );
675 $had_masteriyo_from_promo = get_option( $this->option_masteriyo, false );
676 $has_masteriyo_conditions = $is_min_php_7_2 && ! $had_masteriyo_from_promo && ! $this->has_active_lms_plugin() && $can_check_plugin_install && $this->has_lms_tagline();
677 $has_easy_mcp = defined( 'EASY_MCP_AI_VERSION' ) || $this->any_plugin_dir_exists( array( 'easy-mcp-ai' ) );
678 $had_easy_mcp_from_promo = get_option( $this->option_easy_mcp, false );
679 $has_easy_mcp_base = $is_min_php_7_4 && ! $has_easy_mcp && ! $had_easy_mcp_from_promo && version_compare( get_bloginfo( 'version' ), '7.0', '>=' );
680 $has_easy_mcp_conditions = $has_easy_mcp_base && $can_check_plugin_install && ! $this->has_mcp_plugin_installed() && $this->has_ai_usage_signal();
681 // On the own-profile screen the Application Passwords section itself is the intent signal.
682 $has_easy_mcp_profile_conditions = $this->can_check_profile_promo() && $has_easy_mcp_base && wp_is_application_passwords_available_for_user( wp_get_current_user() ) && ! $this->has_mcp_plugin_installed();
683
684 $all = [
685 'optimole' => [
686 'om-editor' => [
687 'env' => ! $has_optimole && $is_min_req_v && ! $had_optimole_from_promo,
688 'screen' => 'editor',
689 'delayed' => true,
690 ],
691 'om-image-block' => [
692 'env' => ! $has_optimole && $is_min_req_v && ! $had_optimole_from_promo,
693 'screen' => 'editor',
694 'delayed' => true,
695 ],
696 'om-attachment' => [
697 'env' => ! $has_optimole && ! $had_optimole_from_promo,
698 'screen' => 'media-editor',
699 ],
700 'om-media' => [
701 'env' => ! $has_optimole && ! $had_optimole_from_promo && $has_enough_attachments,
702 'screen' => 'media',
703 ],
704 'om-elementor' => [
705 'env' => ! $has_optimole && ! $had_optimole_from_promo && defined( 'ELEMENTOR_VERSION' ),
706 'screen' => 'elementor',
707 'delayed' => true,
708 ],
709 ],
710 'feedzy_import' => [
711 'feedzy-import' => [
712 'env' => true,
713 'screen' => 'import',
714 'always' => true,
715 ],
716 ],
717 'feedzy_embed' => [
718 'feedzy-editor' => [
719 'env' => ! $has_feedzy && is_main_site() && ! $had_feedzy_from_promo,
720 'screen' => 'editor',
721 ],
722 ],
723 'otter' => [
724 'blocks-css' => [
725 'env' => ! $has_otter && $is_min_req_v && ! $had_otter_from_promo,
726 'screen' => 'editor',
727 'delayed' => true,
728 ],
729 'blocks-animation' => [
730 'env' => ! $has_otter && $is_min_req_v && ! $had_otter_from_promo,
731 'screen' => 'editor',
732 'delayed' => true,
733 ],
734 'blocks-conditions' => [
735 'env' => ! $has_otter && $is_min_req_v && ! $had_otter_from_promo,
736 'screen' => 'editor',
737 'delayed' => true,
738 ],
739 ],
740 'rop' => [
741 'rop-posts' => [
742 'env' => ! $has_rop && ! $had_rop_from_promo && $has_enough_old_posts,
743 'screen' => 'edit-post',
744 'delayed' => true,
745 ],
746 ],
747 'woo_plugins' => [
748 'ppom' => [
749 'env' => ! $has_ppom && $has_woocommerce,
750 'screen' => 'edit-product',
751 ],
752 'sparks-wishlist' => [
753 'env' => ! $has_sparks && $has_woocommerce,
754 'screen' => 'edit-product',
755 ],
756 'sparks-announcement' => [
757 'env' => ! $has_sparks && $has_woocommerce,
758 'screen' => 'edit-product',
759 ],
760 'sparks-product-review' => [
761 'env' => ! $has_sparks && $has_woocommerce,
762 'screen' => 'edit-product',
763 ],
764 ],
765 'neve' => [
766 'neve-themes-popular' => [
767 'env' => ! $has_neve && ! $has_neve_from_promo,
768 'screen' => 'themes-install-popular',
769 ],
770 ],
771 'redirection-cf7' => [
772 'wpcf7' => [
773 'env' => ! $has_redirection_cf7 && ! $had_redirection_cf7_promo,
774 'screen' => 'wpcf7',
775 'delayed' => true,
776 ],
777 ],
778 'hyve' => [
779 'hyve-plugins-install' => [
780 'env' => $has_hyve_conditions,
781 'screen' => 'plugin-install',
782 ],
783 ],
784 'wp_full_pay' => [
785 'wp-full-pay-plugins-install' => [
786 'env' => $has_wfp_conditions,
787 'screen' => 'plugin-install',
788 ],
789 ],
790 'learning-management-system' => [
791 'masteriyo-plugins-install' => [
792 'env' => $has_masteriyo_conditions,
793 'screen' => 'plugin-install',
794 ],
795 ],
796 'easy-mcp' => [
797 'easy-mcp-plugins-install' => [
798 'env' => $has_easy_mcp_conditions,
799 'screen' => 'plugin-install',
800 ],
801 'easy-mcp-profile' => [
802 'env' => $has_easy_mcp_profile_conditions,
803 'screen' => 'profile',
804 ],
805 ],
806 ];
807
808 foreach ( $all as $slug => $data ) {
809 foreach ( $data as $key => $conditions ) {
810 if ( ! $conditions['env'] || $this->has_conflicts() ) {
811 unset( $all[ $slug ][ $key ] );
812
813 continue;
814 }
815
816 if ( $this->get_upsells_dismiss_time( $key ) ) {
817 unset( $all[ $slug ][ $key ] );
818 }
819 }
820
821 if ( empty( $all[ $slug ] ) ) {
822 unset( $all[ $slug ] );
823 }
824 }
825 return $all;
826 }
827
828 /**
829 * Get the upsell dismiss time.
830 *
831 * @param string $key The upsell key. If empty will return all dismiss times.
832 *
833 * @return false | string | array
834 */
835 private function get_upsells_dismiss_time( $key = '' ) {
836 $old = get_option( 'themeisle_sdk_promotions_otter', '{}' );
837 $data = get_option( $this->option_main, $old );
838
839 $data = json_decode( $data, true );
840
841 if ( empty( $key ) ) {
842 return $data;
843 }
844
845 return isset( $data[ $key ] ) ? $data[ $key ] : false;
846 }
847
848 /**
849 * Get the last dismiss time of a promotion.
850 *
851 * @return int | false The timestamp of last dismiss or false.
852 */
853 private function get_last_dismiss_time() {
854 $dismissed = $this->get_upsells_dismiss_time();
855
856 return empty( $dismissed ) ? false : max( array_values( $dismissed ) );
857 }
858
859 /**
860 * Filter by screen & merge into single array of keys.
861 *
862 * @return array
863 */
864 private function filter_by_screen_and_merge() {
865 $current_screen = get_current_screen();
866
867 $is_elementor = isset( $_GET['action'] ) && $_GET['action'] === 'elementor';
868 $is_media = isset( $current_screen->id ) && $current_screen->id === 'upload';
869 $is_posts = isset( $current_screen->id ) && $current_screen->id === 'edit-post';
870 $is_editor = method_exists( $current_screen, 'is_block_editor' ) && $current_screen->is_block_editor();
871 $is_theme_install = isset( $current_screen->id ) && ( $current_screen->id === 'theme-install' );
872 $is_plugin_install = isset( $current_screen->id ) && ( $current_screen->id === 'plugin-install' );
873 $is_profile = isset( $current_screen->id ) && $current_screen->id === 'profile';
874 $is_product = isset( $current_screen->id ) && $current_screen->id === 'product';
875 $is_import = isset( $current_screen->id ) && $current_screen->id === 'import';
876 $is_cf7_install = isset( $current_screen->id ) && function_exists( 'str_contains' ) ? str_contains( $current_screen->id, 'page_wpcf7' ) : false;
877
878 $return = [];
879 $product_install_time = (int) $this->product->get_install_time();
880 $is_older = time() > ( $product_install_time + ( 3 * DAY_IN_SECONDS ) );
881 $is_newer = time() < ( $product_install_time + ( 6 * HOUR_IN_SECONDS ) );
882 foreach ( $this->promotions as $slug => $promos ) {
883 if ( ! is_array( $promos ) ) {
884 continue;
885 }
886
887 foreach ( $promos as $key => $data ) {
888 if ( ! is_array( $data ) || ! array_key_exists( 'screen', $data ) ) {
889 unset( $this->promotions[ $slug ][ $key ] );
890
891 continue;
892 }
893
894 $data = wp_parse_args(
895 $data,
896 [
897 'delayed' => false,
898 'always' => false,
899 ]
900 );
901
902 if (
903 ! $this->debug &&
904 (
905 ( $data['delayed'] === true && ! $is_older ) || // Skip promotions that are delayed for 3 days.
906 $is_newer // Skip promotions for the first 6 hours after install.
907 )
908 && ! $data['always']
909 ) {
910 unset( $this->promotions[ $slug ][ $key ] );
911
912 continue;
913 }
914 switch ( $data['screen'] ) {
915 case 'media-editor':
916 if ( ! $is_media && ! $is_editor ) {
917 unset( $this->promotions[ $slug ][ $key ] );
918 }
919 break;
920 case 'media':
921 if ( ! $is_media ) {
922 unset( $this->promotions[ $slug ][ $key ] );
923 }
924 break;
925 case 'editor':
926 if ( ! $is_editor || $is_elementor ) {
927 unset( $this->promotions[ $slug ][ $key ] );
928 }
929 break;
930 case 'import':
931 if ( ! $is_import ) {
932 unset( $this->promotions[ $slug ][ $key ] );
933 }
934 break;
935 case 'elementor':
936 if ( ! $is_elementor ) {
937 unset( $this->promotions[ $slug ][ $key ] );
938 }
939 break;
940 case 'edit-post':
941 if ( ! $is_posts ) {
942 unset( $this->promotions[ $slug ][ $key ] );
943 }
944 break;
945 case 'edit-product':
946 if ( ! $is_product ) {
947 unset( $this->promotions[ $slug ][ $key ] );
948 }
949 break;
950 case 'themes-install-popular':
951 if ( ! $is_theme_install ) {
952 unset( $this->promotions[ $slug ][ $key ] );
953 }
954 break;
955 case 'wpcf7':
956 if ( ! $is_cf7_install ) {
957 unset( $this->promotions[ $slug ][ $key ] );
958 }
959 break;
960 case 'plugin-install':
961 if ( ! $is_plugin_install ) {
962 unset( $this->promotions[ $slug ][ $key ] );
963 }
964 break;
965 case 'profile':
966 if ( ! $is_profile ) {
967 unset( $this->promotions[ $slug ][ $key ] );
968 }
969 break;
970 }
971 }
972
973 $return = array_merge( $return, $this->promotions[ $slug ] );
974 }
975
976 $return = array_filter(
977 $return,
978 function ( $value, $key ) {
979 return ! in_array( $key, $this->dissallowed_promotions, true );
980 },
981 ARRAY_FILTER_USE_BOTH
982 );
983
984 return array_keys( $return );
985 }
986
987 /**
988 * Load single promotion.
989 *
990 * @param string $slug slug of the promotion.
991 */
992 private function load_promotion( $slug ) {
993 $this->loaded_promo = $slug;
994
995 if ( $this->debug ) {
996 add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue' ] );
997 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue' ] );
998 if ( $this->get_upsells_dismiss_time( 'om-media' ) === false ) {
999 add_action( 'admin_notices', [ $this, 'render_optimole_dash_notice' ] );
1000 }
1001 if ( $this->get_upsells_dismiss_time( 'rop-posts' ) === false ) {
1002 add_action( 'admin_notices', [ $this, 'render_rop_dash_notice' ] );
1003 }
1004 if ( $this->get_upsells_dismiss_time( 'neve-themes-popular' ) === false ) {
1005 add_action( 'admin_notices', [ $this, 'render_neve_themes_notice' ] );
1006 }
1007 if ( $this->get_upsells_dismiss_time( 'redirection-cf7' ) === false ) {
1008 add_action( 'admin_notices', [ $this, 'render_redirection_cf7_notice' ] );
1009 }
1010 if ( $this->get_upsells_dismiss_time( 'hyve-plugins-install' ) === false ) {
1011 add_action( 'admin_notices', [ $this, 'render_hyve_notice' ] );
1012 }
1013
1014 if ( $this->get_upsells_dismiss_time( 'wp-full-pay-plugins-install' ) === false ) {
1015 add_action( 'admin_notices', [ $this, 'render_wp_full_pay_notice' ] );
1016 }
1017
1018 if ( $this->get_upsells_dismiss_time( 'masteriyo-plugins-install' ) === false ) {
1019 add_action( 'admin_notices', [ $this, 'render_masteriyo_notice' ] );
1020 }
1021
1022 if ( $this->get_upsells_dismiss_time( 'easy-mcp-plugins-install' ) === false || $this->get_upsells_dismiss_time( 'easy-mcp-profile' ) === false ) {
1023 add_action( 'admin_notices', [ $this, 'render_easy_mcp_notice' ] );
1024 }
1025
1026 add_action( 'load-import.php', [ $this, 'add_import' ] );
1027 $this->load_woo_promos();
1028
1029 return;
1030 }
1031 switch ( $slug ) {
1032 case 'om-editor':
1033 case 'om-image-block':
1034 case 'blocks-css':
1035 case 'blocks-animation':
1036 case 'blocks-conditions':
1037 add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue' ] );
1038 break;
1039 case 'om-attachment':
1040 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue' ] );
1041 break;
1042 case 'om-media':
1043 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue' ] );
1044 add_action( 'admin_notices', [ $this, 'render_optimole_dash_notice' ] );
1045 break;
1046 case 'rop-posts':
1047 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue' ] );
1048 add_action( 'admin_notices', [ $this, 'render_rop_dash_notice' ] );
1049 break;
1050 case 'feedzy-import':
1051 add_action( 'load-import.php', [ $this, 'add_import' ] );
1052
1053 break;
1054 case 'feedzy-editor':
1055 add_action( 'enqueue_block_editor_assets', [ $this, 'enqueue' ] );
1056 break;
1057 case 'ppom':
1058 case 'sparks-wishlist':
1059 case 'sparks-announcement':
1060 case 'sparks-product-reviews':
1061 $this->load_woo_promos();
1062 break;
1063 case 'neve-themes-popular':
1064 // Remove any other notifications if Neve promotion is showing
1065 remove_action( 'admin_notices', array( 'ThemeisleSDK\Modules\Notification', 'show_notification' ) );
1066 remove_action(
1067 'wp_ajax_themeisle_sdk_dismiss_notice',
1068 array(
1069 'ThemeisleSDK\Modules\Notification',
1070 'dismiss',
1071 )
1072 );
1073 remove_action( 'admin_head', array( 'ThemeisleSDK\Modules\Notification', 'dismiss_get' ) );
1074 remove_action( 'admin_head', array( 'ThemeisleSDK\Modules\Notification', 'setup_notifications' ) );
1075 // Add required actions to display this notification
1076 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue' ] );
1077 add_action( 'admin_notices', [ $this, 'render_neve_themes_notice' ] );
1078 break;
1079 case 'wpcf7':
1080 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue' ] );
1081 add_action( 'admin_notices', [ $this, 'render_redirection_cf7_notice' ] );
1082 break;
1083 case 'hyve-plugins-install':
1084 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue' ] );
1085 add_action( 'admin_notices', [ $this, 'render_hyve_notice' ] );
1086 break;
1087 case 'wp-full-pay-plugins-install':
1088 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue' ] );
1089 add_action( 'admin_notices', [ $this, 'render_wp_full_pay_notice' ] );
1090 break;
1091 case 'masteriyo-plugins-install':
1092 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue' ] );
1093 add_action( 'admin_notices', [ $this, 'render_masteriyo_notice' ] );
1094 break;
1095 case 'easy-mcp-plugins-install':
1096 case 'easy-mcp-profile':
1097 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue' ] );
1098 add_action( 'admin_notices', [ $this, 'render_easy_mcp_notice' ] );
1099 break;
1100 }
1101 }
1102
1103 /**
1104 * Add import row.
1105 *
1106 * @return void
1107 */
1108 public function add_import() {
1109 global $wp_importers;
1110 if ( isset( $wp_importers['feedzy-rss-feeds'] ) ) {
1111 return;
1112 }
1113 $wp_importers['feedzy-rss-feeds'] = array( // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
1114 'Feedzy',
1115 sprintf( Loader::$labels['promotions']['feedzy']['import_desc'], '<span style="float: left; font-style: italic;margin-top:0.4em;">', $this->product->get_friendly_name(), '</span>' ),
1116 'install' => 'feedzy-rss-feeds',
1117 );
1118 if ( defined( 'FEEDZY_BASEFILE' ) ) {
1119 unset( $wp_importers['feedzy-rss-feeds']['install'] );
1120 }
1121 }
1122 /**
1123 * Render dashboard notice.
1124 */
1125 public function render_optimole_dash_notice() {
1126 $screen = get_current_screen();
1127
1128 if ( ! isset( $screen->id ) || $screen->id !== 'upload' ) {
1129 return;
1130 }
1131
1132 echo '<div id="ti-optml-notice" class="notice notice-info ti-sdk-om-notice"></div>';
1133 }
1134
1135 /**
1136 * Enqueue the assets.
1137 */
1138 public function enqueue() {
1139 global $themeisle_sdk_max_path;
1140 $handle = 'ti-sdk-promo';
1141 $saved = $this->get_upsells_dismiss_time();
1142 $themeisle_sdk_src = $this->get_sdk_uri();
1143 $user = wp_get_current_user();
1144 $asset_file = require $themeisle_sdk_max_path . '/assets/js/build/promos/index.asset.php';
1145 $deps = array_merge( $asset_file['dependencies'], [ 'updates' ] );
1146
1147 $themes = wp_get_themes();
1148 $neve_action = isset( $themes['neve'] ) ? 'activate' : 'install';
1149 $labels = Loader::$labels['promotions'];
1150 $labels['feedzy']['editor_recommends'] = sprintf(
1151 $labels['feedzy']['editor_recommends'],
1152 $this->product->get_friendly_name(),
1153 '<a target="_blank" href="' . add_query_arg(
1154 array(
1155 'tab' => 'plugin-information',
1156 'plugin' => 'feedzy-rss-feeds',
1157 '_wpnonce' => wp_create_nonce( 'activate-plugin_feedzy-rss-feeds' ),
1158 'feedzy_reference_key' => 'e-' . $this->product->get_key(),
1159 ),
1160 network_admin_url( 'plugin-install.php' )
1161 ) . '">',
1162 '</a>'
1163 );
1164 wp_register_script( $handle, $themeisle_sdk_src . 'assets/js/build/promos/index.js', $deps, $asset_file['version'], true );
1165 wp_localize_script(
1166 $handle,
1167 'themeisleSDKPromotions',
1168 [
1169 'debug' => $this->debug,
1170 'labels' => $labels,
1171 'email' => $user->user_email,
1172 'showPromotion' => $this->loaded_promo,
1173 'optionKey' => $this->option_main,
1174 'product' => $this->product->get_name(),
1175 'option' => empty( $saved ) ? new \stdClass() : $saved,
1176 'nonce' => wp_create_nonce( 'wp_rest' ),
1177 'assets' => $themeisle_sdk_src . 'assets/images/',
1178 'optimoleApi' => esc_url( rest_url( 'optml/v1/register_service' ) ),
1179 'optimoleActivationUrl' => $this->get_plugin_activation_link( 'optimole-wp' ),
1180 'otterActivationUrl' => $this->get_plugin_activation_link( 'otter-blocks' ),
1181 'ropActivationUrl' => $this->get_plugin_activation_link( 'tweet-old-post' ),
1182 'optimoleDash' => esc_url( add_query_arg( [ 'page' => 'optimole' ], admin_url( 'upload.php' ) ) ),
1183 'ropDash' => esc_url( add_query_arg( [ 'page' => 'TweetOldPost' ], admin_url( 'admin.php' ) ) ),
1184 // translators: %s is the product name.
1185 'title' => esc_html( sprintf( Loader::$labels['promotions']['recommended'], $this->product->get_name() ) ),
1186 'redirectionCF7MoreUrl' => tsdk_utmify( 'https://docs.themeisle.com/collection/2014-redirection-for-contact-form-7', 'redirection-for-contact-form-7', 'plugin-install' ),
1187 'rfCF7ActivationUrl' => $this->get_plugin_activation_link( 'wpcf7-redirect' ),
1188 'cf7Dash' => esc_url( add_query_arg( [ 'page' => 'wpcf7-new' ], admin_url( 'admin.php' ) ) ),
1189 'hyveActivationUrl' => $this->get_plugin_activation_link( 'hyve-lite' ),
1190 'hyveDash' => esc_url( add_query_arg( [ 'page' => 'wpfs-settings-stripe' ], admin_url( 'admin.php' ) ) ),
1191 'wpFullPayActivationUrl' => $this->get_plugin_activation_link( 'wp-full-stripe-free' ),
1192 'wpFullPayDash' => esc_url( add_query_arg( [ 'page' => 'wpfs-settings-stripe' ], admin_url( 'admin.php' ) ) ),
1193 'masteriyoActivationUrl' => $this->get_plugin_activation_link( 'masteriyo' ),
1194 'masteriyoDash' => esc_url( add_query_arg( [ 'page' => 'masteriyo-onboard' ], admin_url( 'index.php' ) ) ),
1195 'easyMcpActivationUrl' => add_query_arg( 'easy_mcp_reference_key', 'n-' . $this->product->get_key(), remove_query_arg( 'optimole_reference_key', $this->get_plugin_activation_link( 'easy-mcp-ai' ) ) ),
1196 'easyMcpDash' => esc_url( add_query_arg( [ 'page' => 'easy-mcp-ai' ], admin_url( 'admin.php' ) ) ),
1197 'nevePreviewURL' => esc_url( add_query_arg( [ 'theme' => 'neve' ], admin_url( 'theme-install.php' ) ) ),
1198 'neveAction' => $neve_action,
1199 'activateNeveURL' => esc_url(
1200 add_query_arg(
1201 [
1202 'action' => 'activate',
1203 'stylesheet' => 'neve',
1204 '_wpnonce' => wp_create_nonce( 'switch-theme_neve' ),
1205 ],
1206 admin_url( 'themes.php' )
1207 )
1208 ),
1209 ]
1210 );
1211 wp_enqueue_script( $handle );
1212 wp_enqueue_style( $handle, $themeisle_sdk_src . 'assets/js/build/promos/style-index.css', [ 'wp-components' ], $asset_file['version'] );
1213 }
1214
1215 /**
1216 * Render rop notice.
1217 */
1218 public function render_rop_dash_notice() {
1219 $screen = get_current_screen();
1220
1221 if ( ! isset( $screen->id ) || $screen->id !== 'edit-post' ) {
1222 return;
1223 }
1224
1225 echo '<div id="ti-rop-notice" class="notice notice-info ti-sdk-rop-notice"></div>';
1226 }
1227
1228 /**
1229 * Render Neve Themes notice.
1230 */
1231 public function render_neve_themes_notice() {
1232 echo '<div id="ti-neve-notice" class="notice notice-info ti-sdk-om-notice"></div>';
1233 }
1234
1235 /**
1236 * Render Hyve notice.
1237 */
1238 public function render_hyve_notice() {
1239 echo '<div id="ti-hyve-notice" class="notice notice-info ti-sdk-om-notice"></div>';
1240 }
1241
1242 /**
1243 * Render WP Full Pay notice.
1244 */
1245 public function render_wp_full_pay_notice() {
1246 echo '<div id="ti-wp-full-pay-notice" class="notice notice-info ti-sdk-om-notice"></div>';
1247 }
1248
1249 /**
1250 * Render Redirection for CF7 notice.
1251 */
1252 public function render_redirection_cf7_notice() {
1253 echo '<div id="ti-redirection-cf7-notice" class="notice notice-info ti-sdk-om-notice"></div>';
1254 }
1255
1256 /**
1257 * Render Masteriyo notice.
1258 */
1259 public function render_masteriyo_notice() {
1260 echo '<div id="ti-masteriyo-notice" class="notice notice-info ti-sdk-om-notice"></div>';
1261 }
1262
1263 /**
1264 * Render Easy MCP AI notice.
1265 */
1266 public function render_easy_mcp_notice() {
1267 // Cards are injected only when a paid product loaded Featured_plugins; only then can this request already show one.
1268 if ( apply_filters( 'themeisle_sdk_plugin_api_filter_registered', false ) && $this->is_easy_mcp_card_request() ) {
1269 return;
1270 }
1271
1272 echo '<div id="ti-easy-mcp-notice" class="notice notice-info ti-sdk-om-notice"></div>';
1273 }
1274
1275 /**
1276 * Add promo to attachment modal.
1277 *
1278 * @param array $fields Fields array.
1279 * @param \WP_Post $post Post object.
1280 *
1281 * @return array
1282 */
1283 public function add_attachment_field( $fields, $post ) {
1284 if ( $post->post_type !== 'attachment' ) {
1285 return $fields;
1286 }
1287
1288 if ( ! isset( $post->post_mime_type ) || strpos( $post->post_mime_type, 'image' ) === false ) {
1289 return $fields;
1290 }
1291
1292 $meta = wp_get_attachment_metadata( $post->ID );
1293
1294 if ( isset( $meta['filesize'] ) && $meta['filesize'] < 100000 ) {
1295 return $fields;
1296 }
1297
1298 $fields['optimole'] = array(
1299 'input' => 'html',
1300 'html' => '<div id="ti-optml-notice-helper"></div>',
1301 'label' => '',
1302 );
1303
1304 if ( count( $fields ) < 2 ) {
1305 add_filter( 'wp_required_field_message', '__return_empty_string' );
1306 }
1307
1308 return $fields;
1309 }
1310
1311 /**
1312 * Check if has 50 image media items.
1313 *
1314 * @return bool
1315 */
1316 private function has_min_media_attachments() {
1317 if ( $this->debug ) {
1318 return true;
1319 }
1320 $attachment_count = get_transient( 'tsk_attachment_count' );
1321 if ( false === $attachment_count ) {
1322 $args = array(
1323 'post_type' => 'attachment',
1324 'posts_per_page' => 51,
1325 'fields' => 'ids',
1326 'post_status' => 'inherit',
1327 'no_found_rows' => true,
1328 );
1329
1330 $query = new \WP_Query( $args );
1331 $attachment_count = $query->post_count;
1332
1333
1334 set_transient( 'tsk_attachment_count', $attachment_count, DAY_IN_SECONDS );
1335 }
1336
1337 return $attachment_count > 50;
1338 }
1339
1340 /**
1341 * Check if the website has more than 100 posts and over 10 are over a year old.
1342 *
1343 * @return bool
1344 */
1345 private function has_old_posts() {
1346 if ( $this->debug ) {
1347 return true;
1348 }
1349
1350 $posts_count = get_transient( 'tsk_posts_count' );
1351
1352 // Create a new WP_Query object to get all posts
1353 $args = array(
1354 'post_type' => 'post',
1355 'posts_per_page' => 101, //phpcs:ignore WordPress.WP.PostsPerPage.posts_per_page_posts_per_page
1356 'fields' => 'ids',
1357 'no_found_rows' => true,
1358 );
1359
1360 if ( false === $posts_count ) {
1361 $query = new \WP_Query( $args );
1362 $total_posts = $query->post_count;
1363 wp_reset_postdata();
1364
1365 // Count the number of posts older than 1 year
1366 $one_year_ago = gmdate( 'Y-m-d H:i:s', strtotime( '-1 year' ) );
1367 $args['date_query'] = array(
1368 array(
1369 'before' => $one_year_ago,
1370 'inclusive' => true,
1371 ),
1372 );
1373
1374 $query = new \WP_Query( $args );
1375 $old_posts = $query->post_count;
1376 wp_reset_postdata();
1377
1378 $posts_count = array(
1379 'total_posts' => $total_posts,
1380 'old_posts' => $old_posts,
1381 );
1382
1383 set_transient( 'tsk_posts_count', $posts_count, DAY_IN_SECONDS );
1384 }
1385
1386 // Check if there are more than 100 posts and more than 10 old posts
1387 return $posts_count['total_posts'] > 100 && $posts_count['old_posts'] > 10;
1388 }
1389
1390 /**
1391 * Check if should load Woo promos.
1392 *
1393 * @return bool
1394 */
1395 private function load_woo_promos() {
1396 $this->woo_promos = array(
1397 'ppom' => array(
1398 'title' => Loader::$labels['promotions']['woo']['ppom_title'],
1399 'description' => Loader::$labels['promotions']['woo']['ppom_desc'],
1400 'icon' => '<svg width="25" height="25" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1800 1800"><path d="M241.023,324.818c0.252,0,0.505,0.035,0.758,0.035h465.68c17.266,0,31.256-13.99,31.256-31.252 c0-17.262-13.99-31.247-31.256-31.247H351.021h-109.24c-17.258,0-31.252,13.985-31.252,31.247 C210.529,310.605,224.121,324.412,241.023,324.818z"/><path d="M210.529,450.306c0,17.257,13.994,31.252,31.252,31.252h769.451c17.262,0,31.256-13.995,31.256-31.252 c0-17.266-13.994-31.252-31.256-31.252H241.781C224.523,419.054,210.529,433.04,210.529,450.306z"/><path d="M1011.232,575.751H241.781c-8.149,0-15.549,3.147-21.116,8.261c-6.213,5.712-10.136,13.879-10.136,22.987 c0,17.262,13.994,31.26,31.252,31.26h769.451c17.262,0,31.256-13.999,31.256-31.26c0-9.108-3.923-17.275-10.141-22.987 C1026.781,578.898,1019.386,575.751,1011.232,575.751z"/><path d="M1011.232,732.461H241.781c-17.258,0-31.252,13.99-31.252,31.247c0,17.262,13.994,31.257,31.252,31.257 h769.451c17.262,0,31.256-13.995,31.256-31.257C1042.488,746.451,1028.494,732.461,1011.232,732.461z"/><path d="M1011.232,889.157H241.781c-8.149,0-15.549,3.147-21.116,8.261c-6.213,5.713-10.136,13.879-10.136,22.987 c0,17.257,13.994,31.261,31.252,31.261h769.451c17.262,0,31.256-14.004,31.256-31.261c0-9.108-3.923-17.274-10.141-22.987 C1026.781,892.305,1019.386,889.157,1011.232,889.157z"/><path d="M1011.232,1045.867H241.781c-17.258,0-31.252,13.99-31.252,31.243c0,17.271,13.994,31.265,31.252,31.265 h769.451c17.262,0,31.256-13.994,31.256-31.265C1042.488,1059.857,1028.494,1045.867,1011.232,1045.867z"/><path d="M1011.232,1202.576H241.781c-17.258,0-31.252,13.995-31.252,31.252c0,17.258,13.994,31.252,31.252,31.252 h769.451c17.262,0,31.256-13.994,31.256-31.252C1042.488,1216.571,1028.494,1202.576,1011.232,1202.576z"/><path d="M1011.232,1359.273H241.781c-8.149,0-15.549,3.151-21.116,8.265c-6.213,5.713-10.136,13.875-10.136,22.987 c0,17.258,13.994,31.261,31.252,31.261h769.451c17.262,0,31.256-14.003,31.256-31.261c0-9.112-3.923-17.274-10.141-22.987 C1026.781,1362.425,1019.386,1359.273,1011.232,1359.273z"/><path d="M1233.542,251.228l-49.851-45.109L1052.136,87.076l-59.185-53.554c-5.293-4.792-11.947-7.421-18.786-7.836 h-3.49H83.676c-45.688,0-82.858,37.375-82.858,83.316v1583.612c0,45.94,37.17,83.316,82.858,83.316h1078.562 c45.68,0,82.845-37.376,82.845-83.316V277.08v-3.182C1244.646,264.73,1240.261,256.589,1233.542,251.228z M1003.117,125.864 l131.119,118.657h-131.119V125.864z M1183.691,1692.613c0,12.094-9.622,21.926-21.454,21.926H83.676 c-11.836,0-21.467-9.832-21.467-21.926V109.001c0-12.089,9.631-21.925,21.467-21.925h857.857V275.38 c0,17.052,13.785,30.862,30.786,30.862h211.372V1692.613z"/><path d="M1798.578,180.737c-7.049-88.305-81.114-158.02-171.205-158.02c-0.004,0-0.004,0-0.004,0 c-45.889,0-89.033,17.874-121.479,50.32c-29.18,29.175-46.519,67.005-49.73,107.699h-0.586v13.609c0,0.06-0.005,0.115-0.005,0.175 c0,0.026,0.005,0.056,0.005,0.082l-0.005,1369.26h0.197c0.557,5.404,2.522,10.731,6.047,15.373l141.135,185.91 c5.803,7.648,14.851,12.136,24.447,12.136c9.601-0.004,18.646-4.496,24.447-12.14l141.093-185.897 c3.528-4.65,5.494-9.982,6.051-15.391h0.197V180.737H1798.578z M1549.299,116.448c20.854-20.855,48.578-32.339,78.07-32.339h0.004 c50.24,0,92.746,33.723,106.076,79.718h-212.19C1526.358,146.098,1535.896,129.852,1549.299,116.448z M1595.372,1502.468 l-78.413,0.005l0.005-1260.345h220.828v1260.336h-81.103l0.009-1016.486l-61.335,0.004L1595.372,1502.468z M1627.382,1695.821 l-100.171-131.963l200.338-0.004L1627.382,1695.821z"/></svg>',
1401 'has_install' => true,
1402 'link' => wp_nonce_url(
1403 add_query_arg(
1404 array(
1405 'action' => 'install-plugin',
1406 'plugin' => 'woocommerce-product-addon',
1407 ),
1408 admin_url( 'update.php' )
1409 ),
1410 'install-plugin_woocommerce-product-addon'
1411 ),
1412 ),
1413 'sparks-wishlist' => array(
1414 'title' => Loader::$labels['promotions']['woo']['spark_title1'],
1415 'description' => Loader::$labels['promotions']['woo']['spark_desc1'],
1416 'icon' => '<svg width="25" height="25" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/SVG" aria-hidden="true"><path d="M60 21.25H57.5V40.125H60V21.25Z"></path><path d="M2.5 40H0V8.75C0 6.625 1.625 5 3.75 5H25V7.5H3.75C3 7.5 2.5 8 2.5 8.75V40Z"></path><path d="M56.25 51.25H3.75C1.625 51.25 0 49.625 0 47.5V42.5H25V45H2.5V47.5C2.5 48.25 3 48.75 3.75 48.75H56.25C57 48.75 57.5 48.25 57.5 47.5V43.75H60V47.5C60 49.625 58.375 51.25 56.25 51.25Z"></path><path d="M23.75 58.75H21.25V57.25L22.5 51.125V50H25V51.5L23.75 57.625V58.75Z"></path><path d="M38.75 58.75H36.25V57.625L35 51.25V50H37.5V51.125L38.75 57.5V58.75Z"></path><path d="M41.25 57.5H18.75V60H41.25V57.5Z"></path><path d="M56.25 32.5H43.75C41.625 32.5 40 30.875 40 28.75V3.75C40 1.625 41.625 0 43.75 0H56.25C58.375 0 60 1.625 60 3.75V28.75C60 30.875 58.375 32.5 56.25 32.5ZM43.75 2.5C43 2.5 42.5 3 42.5 3.75V28.75C42.5 29.5 43 30 43.75 30H56.25C57 30 57.5 29.5 57.5 28.75V3.75C57.5 3 57 2.5 56.25 2.5H43.75Z"></path><path d="M50 27.5C50.6904 27.5 51.25 26.9404 51.25 26.25C51.25 25.5596 50.6904 25 50 25C49.3096 25 48.75 25.5596 48.75 26.25C48.75 26.9404 49.3096 27.5 50 27.5Z"></path><path d="M51.25 45H31.25C29.125 45 27.5 43.375 27.5 41.25V8.75C27.5 6.625 29.125 5 31.25 5H37.5V7.5H31.25C30.5 7.5 30 8 30 8.75V41.25C30 42 30.5 42.5 31.25 42.5H51.25C52 42.5 52.5 42 52.5 41.25V35H55V41.25C55 43.375 53.375 45 51.25 45Z"></path><path d="M41.25 40C41.9404 40 42.5 39.4404 42.5 38.75C42.5 38.0596 41.9404 37.5 41.25 37.5C40.5596 37.5 40 38.0596 40 38.75C40 39.4404 40.5596 40 41.25 40Z"></path><path d="M21.75 40H18.25L13.25 35H11.75L6.75 40H0V37.5H5.75L10.75 32.5H14.25L19.25 37.5H20.75L52.875 5.375L54.625 7.125L21.75 40Z"></path><path d="M55 11.25H52.5V7.5H48.75V5H55V11.25Z"></path><defs><clip-path id="clip0"><rect width="60" height="60" fill="white"></rect></clip-path></defs></svg>',
1417 'link' => tsdk_utmify( 'https://themeisle.com/plugins/sparks-for-woocommerce/', 'promo', 'products-tabs' ),
1418 ),
1419 'sparks-announcement' => array(
1420 'title' => Loader::$labels['promotions']['woo']['spark_title2'],
1421 'description' => Loader::$labels['promotions']['woo']['spark_desc2'],
1422 'icon' => '<svg width="25" height="25" viewBox="0 0 60 61" fill="none" xmlns="http://www.w3.org/2000/SVG" aria-hidden="true"><path d="M30 8.89282C29.6685 8.89282 29.3505 8.76113 29.1161 8.52671C28.8817 8.29228 28.75 7.97434 28.75 7.64282V1.39282C28.75 1.0613 28.8817 0.743359 29.1161 0.508939C29.3505 0.274518 29.6685 0.142822 30 0.142822C30.3315 0.142822 30.6495 0.274518 30.8839 0.508939C31.1183 0.743359 31.25 1.0613 31.25 1.39282V7.64282C31.25 7.97434 31.1183 8.29228 30.8839 8.52671C30.6495 8.76113 30.3315 8.89282 30 8.89282Z"></path><path d="M30 21.9105L26.25 18.1605V7.82598L26.9409 7.47992C27.8914 7.00723 28.9385 6.76123 30 6.76123C31.0615 6.76123 32.1086 7.00723 33.0591 7.47992L33.75 7.82598V18.1605L30 21.9105ZM28.75 17.1253L30 18.3753L31.25 17.1253V9.44219C30.4344 9.19928 29.5656 9.19928 28.75 9.44219V17.1253Z"></path><path d="M60 60.1428H0V22.6428H17.5C17.8315 22.6428 18.1495 22.7745 18.3839 23.0089C18.6183 23.2434 18.75 23.5613 18.75 23.8928C18.75 24.2243 18.6183 24.5423 18.3839 24.7767C18.1495 25.0111 17.8315 25.1428 17.5 25.1428H2.5V57.6428H57.5V25.1428H42.5C42.1685 25.1428 41.8505 25.0111 41.6161 24.7767C41.3817 24.5423 41.25 24.2243 41.25 23.8928C41.25 23.5613 41.3817 23.2434 41.6161 23.0089C41.8505 22.7745 42.1685 22.6428 42.5 22.6428H60V60.1428Z"></path><path d="M11.2493 53.8933C11.0421 53.8929 10.8383 53.841 10.6561 53.7424C10.474 53.6438 10.3191 53.5015 10.2055 53.3283C10.0919 53.1551 10.0231 52.9564 10.0052 52.75C9.98727 52.5436 10.0209 52.336 10.103 52.1458L26.353 14.6459C26.4182 14.4953 26.5125 14.359 26.6304 14.2448C26.7483 14.1306 26.8876 14.0408 27.0402 13.9804C27.1928 13.9201 27.3559 13.8903 27.52 13.893C27.6841 13.8956 27.8461 13.9306 27.9967 13.9958C28.1473 14.0611 28.2836 14.1553 28.3978 14.2732C28.5119 14.3912 28.6018 14.5304 28.6621 14.683C28.7225 14.8357 28.7522 14.9987 28.7496 15.1628C28.7469 15.3269 28.712 15.4889 28.6467 15.6395L12.3967 53.1395C12.2999 53.3634 12.1397 53.5541 11.9358 53.6881C11.7319 53.822 11.4932 53.8934 11.2493 53.8933Z"></path><path d="M48.7505 53.8935C48.5065 53.8935 48.2679 53.8222 48.064 53.6883C47.8601 53.5543 47.6999 53.3637 47.603 53.1398L31.353 15.6398C31.2212 15.3356 31.2157 14.9915 31.3376 14.6833C31.4595 14.375 31.6989 14.1278 32.003 13.9961C32.3072 13.8643 32.6513 13.8588 32.9595 13.9807C33.2678 14.1026 33.515 14.3419 33.6467 14.6461L49.8967 52.1461C49.9789 52.3363 50.0125 52.5439 49.9946 52.7503C49.9767 52.9566 49.9078 53.1553 49.7942 53.3285C49.6806 53.5018 49.5258 53.6441 49.3436 53.7427C49.1614 53.8413 48.9576 53.8932 48.7505 53.8936V53.8935Z"></path><path d="M30 33.8928C29.6685 33.8928 29.3505 33.7611 29.1161 33.5267C28.8817 33.2923 28.75 32.9743 28.75 32.6428V25.1428C28.75 24.8113 28.8817 24.4934 29.1161 24.2589C29.3505 24.0245 29.6685 23.8928 30 23.8928C30.3315 23.8928 30.6495 24.0245 30.8839 24.2589C31.1183 24.4934 31.25 24.8113 31.25 25.1428V32.6428C31.25 32.9743 31.1183 33.2923 30.8839 33.5267C30.6495 33.7611 30.3315 33.8928 30 33.8928Z"></path><path d="M45 30.1428H15C14.6685 30.1428 14.3505 30.0111 14.1161 29.7767C13.8817 29.5423 13.75 29.2243 13.75 28.8928C13.75 28.5613 13.8817 28.2434 14.1161 28.0089C14.3505 27.7745 14.6685 27.6428 15 27.6428H45C45.3315 27.6428 45.6495 27.7745 45.8839 28.0089C46.1183 28.2434 46.25 28.5613 46.25 28.8928C46.25 29.2243 46.1183 29.5423 45.8839 29.7767C45.6495 30.0111 45.3315 30.1428 45 30.1428Z"></path><defs><clip-path id="clip0"><rect width="60" height="60" fill="white" transform="translate(0 0.142822)"></rect></clip-path></defs></svg>',
1423 'link' => tsdk_utmify( 'https://themeisle.com/plugins/sparks-for-woocommerce/', 'promo', 'products-tabs' ),
1424 ),
1425 'sparks-product-review' => array(
1426 'title' => Loader::$labels['promotions']['woo']['spark_title3'],
1427 'description' => Loader::$labels['promotions']['woo']['spark_desc3'],
1428 'icon' => '<svg width="25" height="25" viewBox="0 0 60 61" fill="none" xmlns="http://www.w3.org/2000/SVG" aria-hidden="true"><path d="M58.75 54.1797H1.25C1.08584 54.1797 0.923271 54.1474 0.771595 54.0846C0.619919 54.0218 0.482103 53.9297 0.366021 53.8137C0.24994 53.6976 0.157867 53.5598 0.0950637 53.4081C0.0322604 53.2564 -4.26571e-05 53.0939 4.22759e-08 52.9297V6.67969C-4.26571e-05 6.51552 0.0322604 6.35296 0.0950637 6.20128C0.157867 6.04961 0.24994 5.91179 0.366021 5.79571C0.482103 5.67963 0.619919 5.58755 0.771595 5.52475C0.923271 5.46195 1.08584 5.42964 1.25 5.42969H58.75C58.9142 5.42964 59.0767 5.46195 59.2284 5.52475C59.3801 5.58755 59.5179 5.67963 59.634 5.79571C59.7501 5.91179 59.8421 6.04961 59.9049 6.20128C59.9677 6.35296 60 6.51552 60 6.67969V52.9297C60 53.0939 59.9677 53.2564 59.9049 53.4081C59.8421 53.5598 59.7501 53.6976 59.634 53.8137C59.5179 53.9297 59.3801 54.0218 59.2284 54.0846C59.0767 54.1474 58.9142 54.1797 58.75 54.1797ZM2.5 51.6797H57.5V7.92969H2.5V51.6797Z"></path><path d="M6.25 15.4297C6.94036 15.4297 7.5 14.87 7.5 14.1797C7.5 13.4893 6.94036 12.9297 6.25 12.9297C5.55964 12.9297 5 13.4893 5 14.1797C5 14.87 5.55964 15.4297 6.25 15.4297Z"></path><path d="M10 15.4297C10.6904 15.4297 11.25 14.87 11.25 14.1797C11.25 13.4893 10.6904 12.9297 10 12.9297C9.30964 12.9297 8.75 13.4893 8.75 14.1797C8.75 14.87 9.30964 15.4297 10 15.4297Z"></path><path d="M13.75 15.4297C14.4404 15.4297 15 14.87 15 14.1797C15 13.4893 14.4404 12.9297 13.75 12.9297C13.0596 12.9297 12.5 13.4893 12.5 14.1797C12.5 14.87 13.0596 15.4297 13.75 15.4297Z"></path><path d="M58.75 15.4297H18.75C18.4185 15.4297 18.1005 15.298 17.8661 15.0636C17.6317 14.8292 17.5 14.5112 17.5 14.1797C17.5 13.8482 17.6317 13.5302 17.8661 13.2958C18.1005 13.0614 18.4185 12.9297 18.75 12.9297H58.75C59.0815 12.9297 59.3995 13.0614 59.6339 13.2958C59.8683 13.5302 60 13.8482 60 14.1797C60 14.5112 59.8683 14.8292 59.6339 15.0636C59.3995 15.298 59.0815 15.4297 58.75 15.4297Z"></path><path d="M28.7502 37.9297C28.4187 37.9295 28.1009 37.7978 27.8664 37.5634L24.4289 34.1259C24.198 33.8908 24.0693 33.574 24.0708 33.2445C24.0723 32.915 24.2039 32.5994 24.4369 32.3664C24.6699 32.1334 24.9855 32.0018 25.315 32.0003C25.6445 31.9988 25.9613 32.1275 26.1964 32.3584L28.6977 34.8597L38.8588 23.4522C38.968 23.3296 39.1002 23.2298 39.2479 23.1583C39.3957 23.0869 39.5561 23.0452 39.7199 23.0358C39.8838 23.0263 40.0479 23.0492 40.2029 23.1032C40.3579 23.1571 40.5008 23.2411 40.6233 23.3503C40.7459 23.4594 40.8457 23.5917 40.9172 23.7394C40.9886 23.8872 41.0303 24.0476 41.0397 24.2114C41.0492 24.3753 41.0263 24.5394 40.9723 24.6944C40.9184 24.8494 40.8344 24.9922 40.7253 25.1148L29.6834 37.511C29.5702 37.6382 29.4322 37.7409 29.2779 37.8129C29.1237 37.8849 28.9563 37.9247 28.7862 37.9298L28.7502 37.9297Z"></path><path d="M29.977 44.1812C28.3217 44.1775 26.6876 43.8085 25.1912 43.1007C23.6948 42.3928 22.3731 41.3635 21.3203 40.0861C20.2675 38.8087 19.5095 37.3148 19.1004 35.7108C18.6913 34.1068 18.6413 32.4322 18.9537 30.8067C19.2662 29.1811 19.9335 27.6445 20.9081 26.3065C21.8827 24.9684 23.1406 23.862 24.592 23.0659C26.0433 22.2699 27.6525 21.804 29.3046 21.7013C30.9568 21.5987 32.6113 21.8619 34.15 22.4722C34.4579 22.5949 34.7044 22.8349 34.8354 23.1393C34.9663 23.4438 34.9709 23.7878 34.8482 24.0957C34.7255 24.4036 34.4856 24.6501 34.1811 24.7811C33.8766 24.912 33.5326 24.9166 33.2247 24.7939C31.44 24.0862 29.472 23.985 27.6241 24.5059C25.7762 25.0269 24.1508 26.141 22.9985 27.6767C21.8462 29.2124 21.2308 31.0844 21.2473 33.0043C21.2637 34.9242 21.9111 36.7854 23.0895 38.3011C24.268 39.8168 25.9122 40.903 27.7688 41.3922C29.6254 41.8813 31.5913 41.7464 33.3637 41.0082C35.136 40.27 36.6164 38.9694 37.5768 37.3069C38.5372 35.6444 38.9242 33.7122 38.6782 31.8081C38.6568 31.6451 38.6678 31.4795 38.7104 31.3208C38.7531 31.1621 38.8267 31.0133 38.927 30.8831C39.0272 30.7528 39.1522 30.6436 39.2947 30.5617C39.4373 30.4798 39.5945 30.4268 39.7575 30.4058C39.9206 30.3848 40.0861 30.3961 40.2448 30.4391C40.4034 30.4822 40.552 30.5561 40.682 30.6566C40.8121 30.7572 40.921 30.8824 41.0026 31.0251C41.0842 31.1678 41.1368 31.3252 41.1574 31.4883C41.3469 32.9535 41.2459 34.4417 40.8602 35.8679C40.4745 37.294 39.8116 38.6303 38.9094 39.8002C38.0072 40.9702 36.8834 41.9509 35.6021 42.6865C34.3208 43.4221 32.9071 43.898 31.4419 44.0872C30.9561 44.1494 30.4668 44.1807 29.977 44.1812Z"></path><defs><clip-path id="clip0"><rect width="60" height="60" fill="white" transform="translate(0 0.429688)"></rect></clip-path></defs></svg>',
1429 'link' => tsdk_utmify( 'https://themeisle.com/plugins/sparks-for-woocommerce/', 'promo', 'products-tabs' ),
1430 ),
1431 );
1432
1433 // Check if $this-promotions isn't empty and has one of the items to load.
1434 $can_load = ! empty( $this->promotions ) && count( array_intersect( $this->promotions, array_keys( $this->woo_promos ) ) ) > 0;
1435
1436 if ( ! $can_load && ! $this->debug ) {
1437 return;
1438 }
1439
1440 add_action(
1441 'woocommerce_product_data_tabs',
1442 function ( $tabs ) {
1443 $tabs['tisdk-suggestions'] = array(
1444 'label' => Loader::$labels['promotions']['woo']['title'],
1445 'target' => 'tisdk_suggestions',
1446 'class' => array(),
1447 'priority' => 1000,
1448 );
1449
1450 return $tabs;
1451 }
1452 );
1453
1454 add_action( 'woocommerce_product_data_panels', array( $this, 'woocommerce_tab_content' ) );
1455 }
1456
1457 /**
1458 * WooCommerce Tab Content.
1459 */
1460 public function woocommerce_tab_content() {
1461 // Filter content based on if the key exists in $this->promotions array.
1462 $content = array_filter(
1463 $this->woo_promos,
1464 function ( $key ) {
1465 return in_array( $key, $this->promotions, true );
1466 },
1467 ARRAY_FILTER_USE_KEY
1468 );
1469
1470 // Display CSS
1471 self::render_woo_tabs_css();
1472
1473 self::render_notice_dismiss_ajax();
1474 ?>
1475
1476 <div id="tisdk_suggestions" class="panel woocommerce_options_panel hidden">
1477 <div class="tisdk-suggestions-header">
1478 <h4><?php echo esc_html( Loader::$labels['promotions']['woo']['title2'] ); ?></h4>
1479 </div>
1480 <div class="tisdk-suggestions-content">
1481 <?php foreach ( $content as $key => $item ) : ?>
1482 <div class="tisdk-suggestion" id="<?php echo esc_attr( $key ); ?>">
1483 <?php if ( isset( $item['icon'] ) ) : ?>
1484 <div class="tisdk-suggestion-icon">
1485 <?php echo $item['icon']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
1486 </div>
1487 <?php endif; ?>
1488 <div class="tisdk-suggestion-content">
1489 <h4><?php echo esc_html( $item['title'] ); ?></h4>
1490 <p><?php echo esc_html( $item['description'] ); ?></p>
1491 </div>
1492 <div class="tisdk-suggestion-cta">
1493 <a href="<?php echo esc_url( $item['link'] ); ?>" target="blank" class="button">
1494 <?php echo( ( isset( $item['has_install'] ) && $item['has_install'] ) ? esc_html( Loader::$labels['promotions']['woo']['cta_install'] ) : esc_html( Loader::$labels['promotions']['woo']['learn_more'] ) ); ?>
1495 </a>
1496 <a class="suggestion-dismiss"
1497 title="<?php echo esc_attr( Loader::$labels['promotions']['woo']['dismiss'] ); ?>"
1498 href="#"></a>
1499 </div>
1500 </div>
1501 <?php endforeach; ?>
1502 </div>
1503 </div>
1504 <?php
1505 }
1506
1507 /**
1508 * CSS for WooCommerce Tabs
1509 */
1510 public static function render_woo_tabs_css() {
1511 $icon = 'M208 88.286c0-10 6.286-21.714 17.715-21.714 11.142 0 17.714 11.714 17.714 21.714 0 10.285-6.572 21.714-17.714 21.714C214.286 110 208 98.571 208 88.286zm304 160c0 36.001-11.429 102.286-36.286 129.714-22.858 24.858-87.428 61.143-120.857 70.572l-1.143.286v32.571c0 16.286-12.572 30.571-29.143 30.571-10 0-19.429-5.714-24.572-14.286-5.427 8.572-14.856 14.286-24.856 14.286-10 0-19.429-5.714-24.858-14.286-5.142 8.572-14.571 14.286-24.57 14.286-10.286 0-19.429-5.714-24.858-14.286-5.143 8.572-14.571 14.286-24.571 14.286-18.857 0-29.429-15.714-29.429-32.857-16.286 12.285-35.715 19.428-56.571 19.428-22 0-43.429-8.285-60.286-22.857 10.285-.286 20.571-2.286 30.285-5.714-20.857-5.714-39.428-18.857-52-36.286 21.37 4.645 46.209 1.673 67.143-11.143-22-22-56.571-58.857-68.572-87.428C1.143 321.714 0 303.714 0 289.429c0-49.714 20.286-160 86.286-160 10.571 0 18.857 4.858 23.143 14.857a158.792 158.792 0 0 1 12-15.428c2-2.572 5.714-5.429 7.143-8.286 7.999-12.571 11.714-21.142 21.714-34C182.571 45.428 232 17.143 285.143 17.143c6 0 12 .285 17.714 1.143C313.714 6.571 328.857 0 344.572 0c14.571 0 29.714 6 40 16.286.857.858 1.428 2.286 1.428 3.428 0 3.714-10.285 13.429-12.857 16.286 4.286 1.429 15.714 6.858 15.714 12 0 2.857-2.857 5.143-4.571 7.143 31.429 27.714 49.429 67.143 56.286 108 4.286-5.143 10.285-8.572 17.143-8.572 10.571 0 20.857 7.144 28.571 14.001C507.143 187.143 512 221.714 512 248.286zM188 89.428c0 18.286 12.571 37.143 32.286 37.143 19.714 0 32.285-18.857 32.285-37.143 0-18-12.571-36.857-32.285-36.857-19.715 0-32.286 18.858-32.286 36.857zM237.714 194c0-19.714 3.714-39.143 8.571-58.286-52.039 79.534-13.531 184.571 68.858 184.571 21.428 0 42.571-7.714 60-20 2-7.429 3.714-14.857 3.714-22.572 0-14.286-6.286-21.428-20.572-21.428-4.571 0-9.143.857-13.429 1.714-63.343 12.668-107.142 3.669-107.142-63.999zm-41.142 254.858c0-11.143-8.858-20.857-20.286-20.857-11.429 0-20 9.715-20 20.857v32.571c0 11.143 8.571 21.142 20 21.142 11.428 0 20.286-9.715 20.286-21.142v-32.571zm49.143 0c0-11.143-8.572-20.857-20-20.857-11.429 0-20.286 9.715-20.286 20.857v32.571c0 11.143 8.857 21.142 20.286 21.142 11.428 0 20-10 20-21.142v-32.571zm49.713 0c0-11.143-8.857-20.857-20.285-20.857-11.429 0-20.286 9.715-20.286 20.857v32.571c0 11.143 8.857 21.142 20.286 21.142 11.428 0 20.285-9.715 20.285-21.142v-32.571zm49.715 0c0-11.143-8.857-20.857-20.286-20.857-11.428 0-20.286 9.715-20.286 20.857v32.571c0 11.143 8.858 21.142 20.286 21.142 11.429 0 20.286-10 20.286-21.142v-32.571zM421.714 286c-30.857 59.142-90.285 102.572-158.571 102.572-96.571 0-160.571-84.572-160.571-176.572 0-16.857 2-33.429 6-49.714-20 33.715-29.714 72.572-29.714 111.429 0 60.286 24.857 121.715 71.429 160.857 5.143-9.714 14.857-16.286 26-16.286 10 0 19.428 5.714 24.571 14.286 5.429-8.571 14.571-14.286 24.858-14.286 10 0 19.428 5.714 24.571 14.286 5.429-8.571 14.857-14.286 24.858-14.286 10 0 19.428 5.714 24.857 14.286 5.143-8.571 14.571-14.286 24.572-14.286 10.857 0 20.857 6.572 25.714 16 43.427-36.286 68.569-92 71.426-148.286zm10.572-99.714c0-53.714-34.571-105.714-92.572-105.714-30.285 0-58.571 15.143-78.857 36.857C240.862 183.812 233.41 254 302.286 254c28.805 0 97.357-28.538 84.286 36.857 28.857-26 45.714-65.714 45.714-104.571z';
1512
1513 ?>
1514 <style>
1515 .tisdk-suggestions_options a {
1516 display: flex !important;
1517 align-items: center;
1518 }
1519
1520 .tisdk-suggestions_options a::before {
1521 content: url("data:image/svg+xml,%3Csvg fill='%23135e96' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath d='<?php echo esc_attr( $icon ); ?>'/%3E%3C/svg%3E") !important;
1522 min-width: 13px;
1523 max-width: 13px;
1524 }
1525
1526 .tisdk-suggestions_options.active a::before {
1527 content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath d='<?php echo esc_attr( $icon ); ?>'/%3E%3C/svg%3E") !important;
1528 }
1529
1530 .tisdk-suggestions-header {
1531 padding: 1em 1.5em;
1532 border-bottom: 1px solid #eee;
1533 }
1534
1535 .tisdk-suggestions-header h4 {
1536 font-size: 1.1em;
1537 margin: 0;
1538 }
1539
1540 .tisdk-suggestion {
1541 display: flex;
1542 align-items: center;
1543 flex-direction: row;
1544 padding: 1em 1.5em;
1545 }
1546
1547 .tisdk-suggestion-icon {
1548 height: 40px;
1549 margin: 0;
1550 margin-right: 0px;
1551 margin-right: 1.5em;
1552 flex: 0 0 40px;
1553 background: #966ccf;
1554 display: flex;
1555 justify-content: center;
1556 border-radius: 100%;
1557 align-items: center;
1558 padding: 5px;
1559 }
1560
1561 .tisdk-suggestion-icon svg {
1562 fill: #fff;
1563 }
1564
1565 .tisdk-suggestion-content {
1566 flex: 1 1 60%;
1567 }
1568
1569 .tisdk-suggestion-content h4 {
1570 margin: 0;
1571 }
1572
1573 .tisdk-suggestion-content p {
1574 margin: 0;
1575 margin-top: 4px;
1576 padding: 0;
1577 line-height: 1.5;
1578 }
1579
1580 .tisdk-suggestion-cta {
1581 flex: 1 1 30%;
1582 min-width: 160px;
1583 text-align: right;
1584 }
1585
1586 .tisdk-suggestion-cta .button {
1587 display: inline-block;
1588 min-width: 120px;
1589 text-align: center;
1590 margin: 0;
1591 }
1592
1593 .tisdk-suggestion-cta .suggestion-dismiss {
1594 position: relative;
1595 top: 5px;
1596 right: auto;
1597 margin-left: 1em;
1598 text-decoration: none;
1599 }
1600 </style>
1601 <?php
1602 }
1603
1604 /**
1605 * JS for Dismissing Notice
1606 */
1607 public static function render_notice_dismiss_ajax() {
1608 ?>
1609 <script>
1610 jQuery(document).ready(function ($) {
1611 // AJAX request to update the option value
1612 $('.tisdk-suggestion .suggestion-dismiss').click(function (e) {
1613 e.preventDefault();
1614 var suggestion = $(this).closest('.tisdk-suggestion');
1615 var value = suggestion.attr('id');
1616
1617 var nonce = '<?php echo esc_attr( wp_create_nonce( 'tisdk_update_option' ) ); ?>';
1618
1619 $.ajax({
1620 url: window.ajaxurl,
1621 type: 'POST',
1622 data: {
1623 action: 'tisdk_update_option',
1624 value,
1625 nonce
1626 },
1627 complete() {
1628 suggestion.remove();
1629
1630 // If element with .tisdk-suggestions-content has no children, hide the whole panel. Skip if the selector doesn't exist.
1631 if ($('.tisdk-suggestions-content').length && !$('.tisdk-suggestions-content').children().length) {
1632 $('.tisdk-suggestions_options').remove();
1633 $('#tisdk_suggestions').remove();
1634 $('.general_options').addClass('active');
1635 $('#general_product_data').css('display', 'block');
1636 }
1637 }
1638 });
1639 });
1640 });
1641 </script>
1642 <?php
1643 }
1644
1645 /**
1646 * Update the option value using AJAX
1647 */
1648 public function dismiss_promotion() {
1649 if ( ! isset( $_POST['nonce'] ) || ! isset( $_POST['value'] ) ) {
1650 $response = array(
1651 'success' => false,
1652 'message' => 'Missing nonce or value.',
1653 );
1654 wp_send_json( $response );
1655 wp_die();
1656 }
1657
1658 $nonce = sanitize_text_field( $_POST['nonce'] );
1659 $value = sanitize_text_field( $_POST['value'] );
1660
1661 if ( ! wp_verify_nonce( $nonce, 'tisdk_update_option' ) ) {
1662 $response = array(
1663 'success' => false,
1664 'message' => 'Invalid nonce.',
1665 );
1666 wp_send_json( $response );
1667 wp_die();
1668 }
1669
1670 $options = get_option( $this->option_main );
1671 $options = json_decode( $options, true );
1672
1673 $options[ $value ] = time();
1674
1675 update_option( $this->option_main, wp_json_encode( $options ) );
1676
1677 $response = array(
1678 'success' => true,
1679 );
1680
1681 wp_send_json( $response );
1682 wp_die();
1683 }
1684
1685 /**
1686 * Check if the user has a support page.
1687 */
1688 public function has_support_page() {
1689 $page_title_matches = $this->get_page_title_keyword_matches();
1690
1691 return 'yes' === $page_title_matches['support'];
1692 }
1693
1694 /**
1695 * Check if the user has a donate page.
1696 */
1697 public function has_donate_page() {
1698 $page_title_matches = $this->get_page_title_keyword_matches();
1699
1700 return 'yes' === $page_title_matches['donate'];
1701 }
1702
1703 /**
1704 * Check if the tagline or a published page title contains LMS related keywords.
1705 *
1706 * @return bool True if LMS-related keywords are found, false otherwise.
1707 */
1708 public function has_lms_tagline() {
1709 $tagline = strtolower( get_bloginfo( 'description' ) );
1710
1711 foreach ( $this->get_lms_keywords() as $keyword ) {
1712 if ( $this->has_lms_keyword_match( $tagline, $keyword ) ) {
1713 return true;
1714 }
1715 }
1716
1717 return $this->has_lms_page_title();
1718 }
1719
1720 /**
1721 * Check if a supported LMS plugin is active.
1722 *
1723 * @return bool True if an LMS plugin is active, false otherwise.
1724 */
1725 private function has_active_lms_plugin() {
1726 if ( defined( 'MASTERIYO_VERSION' ) ) {
1727 return true;
1728 }
1729
1730 include_once ABSPATH . 'wp-admin/includes/plugin.php';
1731
1732 $lms_plugins = array(
1733 'tutor/tutor.php',
1734 'sfwd-lms/sfwd_lms.php',
1735 'lifterlms/lifterlms.php',
1736 'sensei-lms/sensei-lms.php',
1737 'learnpress/learnpress.php',
1738 'masterstudy-lms-learning-management-system/masterstudy-lms-learning-management-system.php',
1739 'learning-management-system/lms.php',
1740 );
1741
1742 foreach ( $lms_plugins as $plugin ) {
1743 if ( is_plugin_active( $plugin ) || ( is_multisite() && is_plugin_active_for_network( $plugin ) ) ) {
1744 return true;
1745 }
1746 }
1747
1748 return false;
1749 }
1750
1751 /**
1752 * Check if the site shows signs of AI usage.
1753 *
1754 * @return bool True if an AI plugin is installed or a REST API application password is in use.
1755 */
1756 public function has_ai_usage_signal() {
1757 return $this->has_ai_plugin_installed() || $this->has_application_password_in_use();
1758 }
1759
1760 /**
1761 * Check if the current plugin-install request already surfaces the Easy MCP AI card,
1762 * so the notice is not stacked on top of it: the Featured tab injects the card on
1763 * WordPress 6.9+ and AI-related searches prepend it.
1764 *
1765 * @return bool True if the Easy MCP AI card is expected on the current request.
1766 */
1767 private function is_easy_mcp_card_request() {
1768 $current_screen = get_current_screen();
1769 if ( ! isset( $current_screen->id ) || 'plugin-install' !== $current_screen->id ) {
1770 return false;
1771 }
1772
1773 // phpcs:disable WordPress.Security.NonceVerification.Recommended
1774 $tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : '';
1775 $search = isset( $_GET['s'] ) ? strtolower( sanitize_text_field( wp_unslash( $_GET['s'] ) ) ) : '';
1776 $paged = isset( $_GET['paged'] ) ? (int) $_GET['paged'] : 1;
1777 // phpcs:enable WordPress.Security.NonceVerification.Recommended
1778
1779 // Cards are only injected on the first results page.
1780 if ( $paged > 1 ) {
1781 return false;
1782 }
1783
1784 if ( '' === $tab || 'featured' === $tab ) {
1785 return version_compare( get_bloginfo( 'version' ), '6.9', '>=' );
1786 }
1787
1788 if ( '' !== $search ) {
1789 return Featured_Plugins::matches_ai_search_keywords( $search ) && ! Featured_Plugins::matches_lms_search_keywords( $search );
1790 }
1791
1792 return false;
1793 }
1794
1795 /**
1796 * Check if a known AI plugin is installed, active or not.
1797 *
1798 * @return bool True if an AI plugin is installed, false otherwise.
1799 */
1800 private function has_ai_plugin_installed() {
1801 return $this->any_plugin_dir_exists(
1802 array(
1803 'ai-engine',
1804 'gpt3-ai-content-generator',
1805 'getgenie',
1806 'bertha-ai',
1807 'hyve',
1808 'hyve-lite',
1809 )
1810 );
1811 }
1812
1813 /**
1814 * Check if any of the given plugin slugs has a directory in the plugins folder.
1815 *
1816 * @param array $slugs Plugin slugs.
1817 *
1818 * @return bool True if at least one plugin directory exists, false otherwise.
1819 */
1820 private function any_plugin_dir_exists( $slugs ) {
1821 foreach ( $slugs as $slug ) {
1822 if ( is_dir( WP_PLUGIN_DIR . '/' . $slug ) ) {
1823 return true;
1824 }
1825 }
1826
1827 return false;
1828 }
1829
1830 /**
1831 * Check if a REST API application password is in use on the site.
1832 *
1833 * @return bool True if an application password is in use, false otherwise.
1834 */
1835 private function has_application_password_in_use() {
1836 return class_exists( 'WP_Application_Passwords' ) && method_exists( 'WP_Application_Passwords', 'is_in_use' ) && \WP_Application_Passwords::is_in_use();
1837 }
1838
1839 /**
1840 * Check if the current request is a profile screen where the Application
1841 * Passwords section lives — a user wiring up an external REST client.
1842 *
1843 * @return bool True if a profile promo can be displayed, false otherwise.
1844 */
1845 private function can_check_profile_promo() {
1846 if ( ! is_admin() || ! current_user_can( 'install_plugins' ) ) {
1847 return false;
1848 }
1849
1850 $current_screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
1851 if ( isset( $current_screen->id ) ) {
1852 return 'profile' === $current_screen->id;
1853 }
1854
1855 global $pagenow;
1856
1857 return isset( $pagenow ) && 'profile.php' === $pagenow;
1858 }
1859
1860 /**
1861 * Check if a competing MCP server plugin is installed, active or not.
1862 *
1863 * AI Engine ships an optional MCP feature but is intentionally kept in the
1864 * AI-usage signals instead — its primary identity is an AI framework.
1865 *
1866 * @return bool True if an MCP server plugin is installed, false otherwise.
1867 */
1868 private function has_mcp_plugin_installed() {
1869 return $this->any_plugin_dir_exists(
1870 array(
1871 'vibe-ai',
1872 'royal-mcp',
1873 'stifli-flex-mcp',
1874 'miniorange-secure-mcp-server',
1875 'wsp-mcp-ai-agents-connector',
1876 'cowboy-mcp',
1877 'nibwp',
1878 )
1879 );
1880 }
1881
1882 /**
1883 * Check if the current request can display a plugin install promo.
1884 *
1885 * @return bool True if a plugin install promo can be displayed, false otherwise.
1886 */
1887 private function can_check_plugin_install_promo() {
1888 if ( ! is_admin() || ! current_user_can( 'install_plugins' ) ) {
1889 return false;
1890 }
1891
1892 $current_screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
1893 if ( isset( $current_screen->id ) ) {
1894 return $current_screen->id === 'plugin-install';
1895 }
1896
1897 global $pagenow;
1898
1899 return isset( $pagenow ) && $pagenow === 'plugin-install.php';
1900 }
1901
1902 /**
1903 * Check if a published page title contains LMS related keywords.
1904 *
1905 * @return bool True if an LMS-related page title is found, false otherwise.
1906 */
1907 private function has_lms_page_title() {
1908 $lms_page_title_signal = get_transient( 'tisdk_lms_page_title_signal_v1' );
1909
1910 if ( in_array( $lms_page_title_signal, array( 'yes', 'no' ), true ) ) {
1911 return 'yes' === $lms_page_title_signal;
1912 }
1913
1914 $has_lms_page_title = $this->has_published_page_title_with_keywords( $this->get_lms_page_title_keywords() );
1915
1916 set_transient( 'tisdk_lms_page_title_signal_v1', $has_lms_page_title ? 'yes' : 'no', 7 * DAY_IN_SECONDS );
1917
1918 return $has_lms_page_title;
1919 }
1920
1921 /**
1922 * Get cached keyword matches for published page titles.
1923 *
1924 * @return array Page title keyword matches.
1925 */
1926 private function get_page_title_keyword_matches() {
1927 $default_matches = array(
1928 'support' => 'no',
1929 'donate' => 'no',
1930 );
1931
1932 $page_title_matches = get_transient( 'tisdk_page_title_signals_v1' );
1933
1934 if ( is_array( $page_title_matches ) && empty( array_diff_key( $default_matches, $page_title_matches ) ) ) {
1935 return array_merge( $default_matches, $page_title_matches );
1936 }
1937
1938 global $wpdb;
1939
1940 $select_clauses = array();
1941 $query_values = array();
1942 $page_checks = $this->get_page_title_checks();
1943
1944 foreach ( $page_checks as $match_key => $keywords ) {
1945 $match_clauses = array();
1946
1947 $query_values[] = 'page';
1948 $query_values[] = 'publish';
1949
1950 foreach ( $keywords as $keyword ) {
1951 $match_clauses[] = 'post_title LIKE %s';
1952 $query_values[] = '%' . $wpdb->esc_like( $keyword ) . '%';
1953 }
1954
1955 $select_clauses[] = 'EXISTS( SELECT 1 FROM ' . $wpdb->posts . ' WHERE post_type = %s AND post_status = %s AND ( ' . implode( ' OR ', $match_clauses ) . ' ) LIMIT 1 ) AS has_' . $match_key;
1956 }
1957
1958 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
1959 $query = $wpdb->get_row( //phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1960 $wpdb->prepare(
1961 'SELECT ' . implode( ', ', $select_clauses ),
1962 $query_values
1963 ),
1964 ARRAY_A
1965 );
1966 // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
1967
1968 $page_title_matches = $default_matches;
1969
1970 if ( is_array( $query ) ) {
1971 foreach ( array_keys( $page_checks ) as $match_key ) {
1972 $page_title_matches[ $match_key ] = ! empty( $query[ 'has_' . $match_key ] ) ? 'yes' : 'no';
1973 }
1974 }
1975
1976 set_transient( 'tisdk_page_title_signals_v1', $page_title_matches, 7 * DAY_IN_SECONDS );
1977
1978 return $page_title_matches;
1979 }
1980
1981 /**
1982 * Check if a keyword matches content.
1983 *
1984 * @param string $content Content to check.
1985 * @param string $keyword Keyword to look for.
1986 *
1987 * @return bool True if the keyword matches, false otherwise.
1988 */
1989 private function has_lms_keyword_match( $content, $keyword ) {
1990 if ( in_array( $keyword, array( 'lms', 'course', 'class' ), true ) ) {
1991 return preg_match( '/(^|[^a-z0-9])' . preg_quote( $keyword, '/' ) . '([^a-z0-9]|$)/', $content ) === 1;
1992 }
1993
1994 return strpos( $content, $keyword ) !== false;
1995 }
1996
1997 /**
1998 * Get LMS related keywords.
1999 *
2000 * @return array LMS related keywords.
2001 */
2002 private function get_lms_keywords() {
2003 return array(
2004 'lms',
2005 'learning',
2006 'course',
2007 'courses',
2008 'academy',
2009 'training',
2010 'lesson',
2011 'lessons',
2012 'class',
2013 'classes',
2014 'student',
2015 'students',
2016 'teach',
2017 'teaching',
2018 'tutor',
2019 'quiz',
2020 'education',
2021 'online course',
2022 'online courses',
2023 );
2024 }
2025
2026 /**
2027 * Get LMS related keywords for page title matching.
2028 *
2029 * @return array LMS related keywords.
2030 */
2031 private function get_lms_page_title_keywords() {
2032 return array_values( array_diff( $this->get_lms_keywords(), array( 'lms', 'course', 'class' ) ) );
2033 }
2034
2035 /**
2036 * Check if a published page title contains any of the provided keywords.
2037 *
2038 * @param array $keywords Keywords to look for.
2039 *
2040 * @return bool True if a matching page title is found, false otherwise.
2041 */
2042 private function has_published_page_title_with_keywords( $keywords ) {
2043 if ( empty( $keywords ) ) {
2044 return false;
2045 }
2046
2047 global $wpdb;
2048
2049 $query_values = array( 'page', 'publish' );
2050 $clauses = array();
2051
2052 foreach ( $keywords as $keyword ) {
2053 $clauses[] = 'post_title LIKE %s';
2054 $query_values[] = '%' . $wpdb->esc_like( $keyword ) . '%';
2055 }
2056
2057 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
2058 $page_title_match = $wpdb->get_var( //phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
2059 $wpdb->prepare(
2060 'SELECT 1 FROM ' . $wpdb->posts . ' WHERE post_type = %s AND post_status = %s AND ( ' . implode( ' OR ', $clauses ) . ' ) LIMIT 1',
2061 $query_values
2062 )
2063 );
2064 // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
2065
2066 return ! empty( $page_title_match );
2067 }
2068
2069 /**
2070 * Get page title keyword checks.
2071 *
2072 * @return array Page title keyword checks.
2073 */
2074 private function get_page_title_checks() {
2075 return array(
2076 'support' => array( 'support' ),
2077 'donate' => array( 'donate' ),
2078 );
2079 }
2080 }
2081