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
← All changes | vendor/codeinwp/themeisle-sdk/src/Modules/Promotions.php +661 -53 4.2.5 → trunk View file →
@@ -112,8 +112,15 @@
112 112 */
113 113 private $option_masteriyo = 'themeisle_sdk_promotions_masteriyo_installed';
114 114
115 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 + /**
116 123 * Loaded promotion.
117 124 *
118 125 * @var string
119 126 */
@@ -119,8 +126,15 @@
119 126 */
120 127 private $loaded_promo;
121 128
122 129 /**
130 + * Whether promotions were processed for the current screen.
131 + *
132 + * @var bool
133 + */
134 + private $available_promotions_loaded = false;
135 +
136 + /**
123 137 * Woo promotions.
124 138 *
125 139 * @var array
126 140 */
@@ -159,8 +173,9 @@
159 173 $promotions_to_load[] = 'hyve';
160 174 $promotions_to_load[] = 'wp_full_pay';
161 175 $promotions_to_load[] = 'feedzy_import';
162 176 $promotions_to_load[] = 'learning-management-system';
177 + $promotions_to_load[] = 'easy-mcp';
163 178
164 179 if ( defined( 'NEVE_VERSION' ) || defined( 'WPMM_PATH' ) || defined( 'OTTER_BLOCKS_VERSION' ) || defined( 'OBFX_URL' ) ) {
165 180 $promotions_to_load[] = 'feedzy_embed';
166 181 }
@@ -201,21 +216,225 @@
201 216
202 217 add_filter( 'attachment_fields_to_edit', array( $this, 'add_attachment_field' ), 10, 2 );
203 218 add_action( 'current_screen', [ $this, 'load_available' ] );
204 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 );
205 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' ) );
206 224 add_filter( 'themeisle_sdk_ran_promos', '__return_true' );
207 225
208 226 if ( get_option( $this->option_neve, false ) !== true ) {
209 - add_action( 'wp_ajax_themeisle_sdk_dismiss_notice', 'ThemeisleSDK\Modules\Notification::regular_dismiss' );
227 + add_action( 'wp_ajax_themeisle_sdk_dismiss_notice', 'ThemeisleSDK\Modules\Notification::dismiss' );
210 228 }
211 229 }
212 230
231 +
213 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 + /**
214 428 * Load available promotions.
215 429 */
216 430 public function load_available() {
217 - $this->promotions = $this->filter_by_screen_and_merge();
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();
218 437 if ( empty( $this->promotions ) ) {
219 438 return;
220 439 }
221 440
@@ -265,8 +484,12 @@
265 484 if ( isset( $_GET['wp_full_pay_reference_key'] ) ) {
266 485 update_option( 'wp_full_pay_reference_key', sanitize_key( $_GET['wp_full_pay_reference_key'] ) );
267 486 }
268 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 +
269 492 if ( isset( $_GET['feedzy_reference_key'] ) || ( isset( $_GET['from'], $_GET['plugin'] ) && $_GET['from'] === 'import' && str_starts_with( sanitize_key( $_GET['plugin'] ), 'feedzy' ) ) ) {
270 493 update_option( 'feedzy_reference_key', sanitize_key( $_GET['feedzy_reference_key'] ?? 'i-' . $this->product->get_key() ) );
271 494 update_option( $this->option_feedzy, 1 );
272 495 }
@@ -368,8 +591,18 @@
368 591 'show_in_rest' => true,
369 592 'default' => false,
370 593 )
371 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 + );
372 605 }
373 606
374 607 /**
375 608 * Check if the path is writable.
@@ -421,14 +654,17 @@
421 654 $has_sparks = defined( 'SPARKS_WC_VERSION' ) || $this->is_plugin_installed( 'sparks-for-woocommerce' );
422 655 $has_ppom = defined( 'PPOM_VERSION' ) || $this->is_plugin_installed( 'woocommerce-product-addon' );
423 656 $has_redirection_cf7 = defined( 'WPCF7_PRO_REDIRECT_PLUGIN_VERSION' ) || $this->is_plugin_installed( 'wpcf7-redirect' );
424 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();
425 661 $has_hyve = defined( 'HYVE_LITE_VERSION' ) || $this->is_plugin_installed( 'hyve' ) || $this->is_plugin_installed( 'hyve-lite' );
426 662 $had_hyve_from_promo = get_option( $this->option_hyve, false );
427 - $has_hyve_conditions = version_compare( get_bloginfo( 'version' ), '6.2', '>=' ) && $this->has_support_page();
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();
428 664 $has_wfp_full_pay = defined( 'WP_FULL_STRIPE_BASENAME' ) || $this->is_plugin_installed( 'wp-full-stripe-free' );
429 665 $had_wfp_from_promo = get_option( $this->option_wp_full_pay, false );
430 - $has_wfp_conditions = $this->has_donate_page();
666 + $has_wfp_conditions = ! $has_wfp_full_pay && ! $had_wfp_from_promo && $can_check_plugin_install && $this->has_donate_page();
431 667 $is_min_req_v = version_compare( get_bloginfo( 'version' ), '5.8', '>=' );
432 668 $current_theme = wp_get_theme();
433 669 $has_neve = $current_theme->template === 'neve' || $current_theme->parent() === 'neve';
434 670 $has_neve_from_promo = get_option( $this->option_neve, false );
@@ -433,15 +669,18 @@
433 669 $has_neve = $current_theme->template === 'neve' || $current_theme->parent() === 'neve';
434 670 $has_neve_from_promo = get_option( $this->option_neve, false );
435 671 $has_enough_attachments = $this->has_min_media_attachments();
436 672 $has_enough_old_posts = $this->has_old_posts();
437 - $is_min_php_7_4 = version_compare( PHP_VERSION, '7.4', '>=' );
438 673 $has_feedzy = defined( 'FEEDZY_BASEFILE' ) || $this->is_plugin_installed( 'feedzy-rss-feedss' );
439 674 $had_feedzy_from_promo = get_option( $this->option_feedzy, false );
440 - $has_masteriyo = defined( 'MASTERIYO_VERSION' ) || $this->is_plugin_installed( 'learning-management-system' );
441 675 $had_masteriyo_from_promo = get_option( $this->option_masteriyo, false );
442 - $has_masteriyo_conditions = $this->has_lms_tagline();
443 - $is_min_php_7_2 = version_compare( PHP_VERSION, '7.2', '>=' );
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();
444 683
445 684 $all = [
446 685 'optimole' => [
447 686 'om-editor' => [
@@ -537,24 +776,34 @@
537 776 ],
538 777 ],
539 778 'hyve' => [
540 779 'hyve-plugins-install' => [
541 - 'env' => $is_min_php_7_4 && ! $has_hyve && ! $had_hyve_from_promo && $has_hyve_conditions,
780 + 'env' => $has_hyve_conditions,
542 781 'screen' => 'plugin-install',
543 782 ],
544 783 ],
545 784 'wp_full_pay' => [
546 785 'wp-full-pay-plugins-install' => [
547 - 'env' => ! $has_wfp_full_pay && ! $had_wfp_from_promo && $has_wfp_conditions,
786 + 'env' => $has_wfp_conditions,
548 787 'screen' => 'plugin-install',
549 788 ],
550 789 ],
551 790 'learning-management-system' => [
552 791 'masteriyo-plugins-install' => [
553 - 'env' => $is_min_php_7_2 && ! $has_masteriyo && ! $had_masteriyo_from_promo && $has_masteriyo_conditions,
792 + 'env' => $has_masteriyo_conditions,
554 793 'screen' => 'plugin-install',
555 794 ],
556 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 + ],
557 806 ];
558 807
559 808 foreach ( $all as $slug => $data ) {
560 809 foreach ( $data as $key => $conditions ) {
@@ -620,8 +869,9 @@
620 869 $is_posts = isset( $current_screen->id ) && $current_screen->id === 'edit-post';
621 870 $is_editor = method_exists( $current_screen, 'is_block_editor' ) && $current_screen->is_block_editor();
622 871 $is_theme_install = isset( $current_screen->id ) && ( $current_screen->id === 'theme-install' );
623 872 $is_plugin_install = isset( $current_screen->id ) && ( $current_screen->id === 'plugin-install' );
873 + $is_profile = isset( $current_screen->id ) && $current_screen->id === 'profile';
624 874 $is_product = isset( $current_screen->id ) && $current_screen->id === 'product';
625 875 $is_import = isset( $current_screen->id ) && $current_screen->id === 'import';
626 876 $is_cf7_install = isset( $current_screen->id ) && function_exists( 'str_contains' ) ? str_contains( $current_screen->id, 'page_wpcf7' ) : false;
627 877
@@ -629,10 +879,19 @@
629 879 $product_install_time = (int) $this->product->get_install_time();
630 880 $is_older = time() > ( $product_install_time + ( 3 * DAY_IN_SECONDS ) );
631 881 $is_newer = time() < ( $product_install_time + ( 6 * HOUR_IN_SECONDS ) );
632 882 foreach ( $this->promotions as $slug => $promos ) {
883 + if ( ! is_array( $promos ) ) {
884 + continue;
885 + }
886 +
633 887 foreach ( $promos as $key => $data ) {
888 + if ( ! is_array( $data ) || ! array_key_exists( 'screen', $data ) ) {
889 + unset( $this->promotions[ $slug ][ $key ] );
634 890
891 + continue;
892 + }
893 +
635 894 $data = wp_parse_args(
636 895 $data,
637 896 [
638 897 'delayed' => false,
@@ -702,8 +961,13 @@
702 961 if ( ! $is_plugin_install ) {
703 962 unset( $this->promotions[ $slug ][ $key ] );
704 963 }
705 964 break;
965 + case 'profile':
966 + if ( ! $is_profile ) {
967 + unset( $this->promotions[ $slug ][ $key ] );
968 + }
969 + break;
706 970 }
707 971 }
708 972
709 973 $return = array_merge( $return, $this->promotions[ $slug ] );
@@ -754,8 +1018,12 @@
754 1018 if ( $this->get_upsells_dismiss_time( 'masteriyo-plugins-install' ) === false ) {
755 1019 add_action( 'admin_notices', [ $this, 'render_masteriyo_notice' ] );
756 1020 }
757 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 +
758 1026 add_action( 'load-import.php', [ $this, 'add_import' ] );
759 1027 $this->load_woo_promos();
760 1028
761 1029 return;
@@ -823,8 +1091,13 @@
823 1091 case 'masteriyo-plugins-install':
824 1092 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue' ] );
825 1093 add_action( 'admin_notices', [ $this, 'render_masteriyo_notice' ] );
826 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;
827 1100 }
828 1101 }
829 1102
830 1103 /**
@@ -918,8 +1191,10 @@
918 1191 'wpFullPayActivationUrl' => $this->get_plugin_activation_link( 'wp-full-stripe-free' ),
919 1192 'wpFullPayDash' => esc_url( add_query_arg( [ 'page' => 'wpfs-settings-stripe' ], admin_url( 'admin.php' ) ) ),
920 1193 'masteriyoActivationUrl' => $this->get_plugin_activation_link( 'masteriyo' ),
921 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' ) ) ),
922 1197 'nevePreviewURL' => esc_url( add_query_arg( [ 'theme' => 'neve' ], admin_url( 'theme-install.php' ) ) ),
923 1198 'neveAction' => $neve_action,
924 1199 'activateNeveURL' => esc_url(
925 1200 add_query_arg(
@@ -985,8 +1260,20 @@
985 1260 echo '<div id="ti-masteriyo-notice" class="notice notice-info ti-sdk-om-notice"></div>';
986 1261 }
987 1262
988 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 + /**
989 1276 * Add promo to attachment modal.
990 1277 *
991 1278 * @param array $fields Fields array.
992 1279 * @param \WP_Post $post Post object.
@@ -1398,75 +1685,396 @@
1398 1685 /**
1399 1686 * Check if the user has a support page.
1400 1687 */
1401 1688 public function has_support_page() {
1402 - $transient_name = 'tisdk_has_support_page';
1403 - $has_support = get_transient( $transient_name );
1689 + $page_title_matches = $this->get_page_title_keyword_matches();
1404 1690
1405 - if ( false === $has_support ) {
1406 - global $wpdb;
1691 + return 'yes' === $page_title_matches['support'];
1692 + }
1407 1693
1408 - // We use %i escape identifier that was added in WP 6.2.0, hence need to ignore PHPCS warning.
1409 - // We only show this notice to users on higher version as that is the minimum for Hyve as well.
1410 - $query = $wpdb->get_var( //phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1411 - $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
1412 - 'SELECT ID FROM %i WHERE post_type = %s AND post_status = %s AND post_title LIKE %s LIMIT 1', // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnsupportedPlaceholder
1413 - $wpdb->posts,
1414 - 'page',
1415 - 'publish',
1416 - '%support%'
1417 - )
1418 - );
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();
1419 1699
1420 - $has_support = $query ? 'yes' : 'no';
1700 + return 'yes' === $page_title_matches['donate'];
1701 + }
1421 1702
1422 - set_transient( $transient_name, $has_support, 7 * DAY_IN_SECONDS );
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 + }
1423 1715 }
1424 1716
1425 - return 'yes' === $has_support;
1717 + return $this->has_lms_page_title();
1426 1718 }
1427 1719
1428 1720 /**
1429 - * Check if the user has a donate page.
1721 + * Check if a supported LMS plugin is active.
1722 + *
1723 + * @return bool True if an LMS plugin is active, false otherwise.
1430 1724 */
1431 - public function has_donate_page() {
1432 - $transient_name = 'tisdk_has_donate_page';
1433 - $has_donate = get_transient( $transient_name );
1725 + private function has_active_lms_plugin() {
1726 + if ( defined( 'MASTERIYO_VERSION' ) ) {
1727 + return true;
1728 + }
1434 1729
1435 - if ( false === $has_donate ) {
1436 - global $wpdb;
1730 + include_once ABSPATH . 'wp-admin/includes/plugin.php';
1437 1731
1438 - $query = $wpdb->get_var( //phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1439 - $wpdb->prepare(
1440 - 'SELECT ID FROM ' . $wpdb->posts . ' WHERE post_type = %s AND post_status = %s AND post_title LIKE %s LIMIT 1',
1441 - 'page',
1442 - 'publish',
1443 - '%donate%'
1444 - )
1445 - );
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 + );
1446 1741
1447 - $has_donate = $query ? 'yes' : 'no';
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 + }
1448 1747
1449 - set_transient( $transient_name, $has_donate, 7 * DAY_IN_SECONDS );
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;
1450 1771 }
1451 1772
1452 - return 'yes' === $has_donate;
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;
1453 1793 }
1454 1794
1455 1795 /**
1456 - * Check if the tagline contains LMS related keywords.
1796 + * Check if a known AI plugin is installed, active or not.
1457 1797 *
1458 - * @return bool True if the tagline contains LMS-related keywords, false otherwise.
1798 + * @return bool True if an AI plugin is installed, false otherwise.
1459 1799 */
1460 - public function has_lms_tagline() {
1461 - $tagline = strtolower( get_bloginfo( 'description' ) );
1462 - $lms_keywords = array( 'learning', 'courses' );
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 + }
1463 1812
1464 - foreach ( $lms_keywords as $keyword ) {
1465 - if ( strpos( $tagline, $keyword ) !== false ) {
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 ) ) {
1466 1823 return true;
1467 1824 }
1468 1825 }
1469 1826
1470 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 + );
1471 2079 }
1472 2080 }