PluginProbe
Visualizer – Tables & Charts Manager with Built-in AI Generator / 4.0.9
Visualizer – Tables & Charts Manager with Built-in AI Generator v4.0.9
4.0.9 4.0.8 4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 3.1.2 3.1.3 3.10.0 3.10.1 3.10.10 3.10.11 3.10.12 3.10.13 3.10.14 3.10.15 3.10.2 All 150 releases
← All changes | vendor/codeinwp/themeisle-sdk/src/Modules/Featured_plugins.php +294 -18 3.10.12 → 4.0.9 View file →
@@ -12,8 +12,9 @@
12 12 */
13 13 namespace ThemeisleSDK\Modules;
14 14
15 15 use ThemeisleSDK\Common\Abstract_Module;
16 +use ThemeisleSDK\Loader;
16 17 use ThemeisleSDK\Product;
17 18
18 19 // Exit if accessed directly.
19 20 if ( ! defined( 'ABSPATH' ) ) {
@@ -32,8 +33,15 @@
32 33 */
33 34 private $transient_key = 'themeisle_sdk_featured_plugins_';
34 35
35 36 /**
37 + * The current product instance.
38 + *
39 + * @var Product|null
40 + */
41 + protected $product = null;
42 +
43 + /**
36 44 * Check if the module can be loaded.
37 45 *
38 46 * @param Product $product Product data.
39 47 *
@@ -43,11 +51,9 @@
43 51 if ( $this->is_from_partner( $product ) ) {
44 52 return false;
45 53 }
46 54
47 - $slug = $product->get_slug();
48 - // only load for products that contain "pro" in the slug.
49 - if ( strpos( $slug, 'pro' ) === false ) {
55 + if ( $product->is_wordpress_available() ) {
50 56 return false;
51 57 }
52 58
53 59 return ! apply_filters( 'themeisle_sdk_disable_featured_plugins', false );
@@ -60,8 +66,10 @@
60 66 *
61 67 * @return void
62 68 */
63 69 public function load( $product ) {
70 + $this->product = $product;
71 +
64 72 if ( ! current_user_can( 'install_plugins' ) ) {
65 73 return;
66 74 }
67 75
@@ -70,12 +78,63 @@
70 78 return;
71 79 }
72 80 add_filter( 'themeisle_sdk_plugin_api_filter_registered', '__return_true' );
73 81
74 - add_filter( 'plugins_api_result', [ $this, 'filter_plugin_api_results' ], 10, 3 );
82 + add_filter( 'plugins_api_result', [ $this, 'filter_plugin_api_results' ], 11, 3 );
83 +
84 + // Enqueue inline JS only on plugin-install.php.
85 + add_action( 'admin_enqueue_scripts', [ $this, 'maybe_add_inline_js' ] );
75 86 }
76 87
77 88 /**
89 + * Enqueue inline JavaScript only on plugin-install.php.
90 + *
91 + * @return void
92 + */
93 + public function maybe_add_inline_js() {
94 + $screen = get_current_screen();
95 + if ( isset( $screen->base ) && 'plugin-install' === $screen->base ) {
96 + add_action(
97 + 'admin_footer',
98 + function() {
99 + $text = esc_html( sprintf( Loader::$labels['promotions']['recommended'], $this->product->get_friendly_name() ) );
100 +
101 + echo '<script>(function(){
102 + function onPluginCardFound(card) {
103 + var recommendedDiv = document.createElement("div");
104 + Object.assign(recommendedDiv.style, {
105 + display: "block",
106 + textAlign: "center",
107 + padding: "0 12px 12px",
108 + background: "#f6f7f7"
109 + });
110 + recommendedDiv.innerHTML = "' . esc_html( $text ) . '";
111 + card.appendChild(recommendedDiv);
112 + }
113 +
114 + function checkAndRun() {
115 + var card = document.querySelector(".plugin-card-learning-management-system");
116 + if (card && !card.dataset.recommendedAdded) {
117 + onPluginCardFound(card);
118 + card.dataset.recommendedAdded = "true";
119 + }
120 + }
121 +
122 + var observer = new MutationObserver(function(mutations) {
123 + checkAndRun();
124 + });
125 +
126 + observer.observe(document.body, { childList: true, subtree: true });
127 +
128 + // Initial check in case the card is already present.
129 + checkAndRun();
130 + })();</script>';
131 + }
132 + );
133 + }
134 + }
135 +
136 + /**
78 137 * Filter the plugin API results to include the featured plugins.
79 138 *
80 139 * @param object $res The result object.
81 140 * @param string $action The type of information being requested from the Plugin Install API.
@@ -88,22 +147,202 @@
88 147 if ( 'query_plugins' !== $action ) {
89 148 return $res;
90 149 }
91 150
151 + if ( isset( $args->page ) && 1 === (int) $args->page && isset( $args->search ) && ! empty( $args->search ) ) {
152 + $original_count = count( (array) $res->plugins );
153 + $res->plugins = $this->maybe_prepend_easy_mcp_plugin( $res->plugins, $args );
154 + $res->plugins = $this->maybe_prepend_lms_plugin( $res->plugins, $args );
155 +
156 + return $this->adjust_results_count( $res, count( (array) $res->plugins ) - $original_count );
157 + }
158 +
92 159 if ( ! isset( $args->browse ) || $args->browse !== 'featured' ) {
93 160 return $res;
94 161 }
95 162
163 + // Inject only on the first page so the same plugins are not repeated on every page.
164 + if ( isset( $args->page ) && (int) $args->page > 1 ) {
165 + return $res;
166 + }
167 +
96 168 $featured = $this->query_plugins_by_author( $args );
97 169
98 - $plugins = array_merge( $featured, (array) $res->plugins );
99 - $plugins = array_slice( $plugins, 0, $res->info['results'] );
100 - $res->plugins = $plugins;
170 + $original_count = count( (array) $res->plugins );
171 + $plugins = $this->remove_plugins_by_slug( (array) $res->plugins, $this->get_plugin_slugs( $featured ) );
172 + $res->plugins = array_merge( $featured, $plugins );
101 173
174 + return $this->adjust_results_count( $res, count( $res->plugins ) - $original_count );
175 + }
176 +
177 + /**
178 + * Prepend the LMS plugin if the search query matches LMS-related terms.
179 + *
180 + * @param array $plugins The plugins array.
181 + * @param object $args The plugin API arguments.
182 + * @return array
183 + */
184 + private function maybe_prepend_lms_plugin( $plugins, $args ) {
185 + $search = isset( $args->search ) ? strtolower( $args->search ) : '';
186 + if ( ! self::matches_lms_search_keywords( $search ) ) {
187 + return $plugins;
188 + }
189 +
190 + $filter_slugs = apply_filters( 'themeisle_sdk_masteriyo_filter_slugs', [ 'learning-management-system' ] );
191 +
192 + return $this->prepend_author_plugins( $plugins, $args, $filter_slugs, 'masteriyo' );
193 + }
194 +
195 + /**
196 + * Prepend the given author's plugins to the list, removing existing copies to avoid duplicates.
197 + *
198 + * @param array $plugins The plugins array.
199 + * @param object $args The plugin API arguments.
200 + * @param array $filter_slugs The slugs to inject.
201 + * @param string $author The wp.org author that owns the slugs.
202 + *
203 + * @return array
204 + */
205 + private function prepend_author_plugins( $plugins, $args, $filter_slugs, $author ) {
206 + $injected = $this->get_plugins_filtered_from_author( $args, $filter_slugs, $author );
207 + if ( empty( $injected ) ) {
208 + return $plugins;
209 + }
210 +
211 + $plugins = $this->remove_plugins_by_slug( (array) $plugins, $this->get_plugin_slugs( $injected ) );
212 +
213 + return array_merge( $injected, $plugins );
214 + }
215 +
216 + /**
217 + * Prepend the Easy MCP AI plugin if the search query matches AI/MCP-related terms.
218 + *
219 + * @param array $plugins The plugins array.
220 + * @param object $args The plugin API arguments.
221 + * @return array
222 + */
223 + private function maybe_prepend_easy_mcp_plugin( $plugins, $args ) {
224 + $search = isset( $args->search ) ? strtolower( $args->search ) : '';
225 +
226 + // The LMS prepend keeps precedence — never stack two injected cards.
227 + if ( self::matches_lms_search_keywords( $search ) || ! self::matches_ai_search_keywords( $search ) ) {
228 + return $plugins;
229 + }
230 +
231 + $filter_slugs = apply_filters( 'themeisle_sdk_easy_mcp_filter_slugs', [ 'easy-mcp-ai' ] );
232 +
233 + return $this->prepend_author_plugins( $plugins, $args, $filter_slugs, 'easymcpai' );
234 + }
235 +
236 + /**
237 + * Check if a plugin search query contains AI/MCP terms or supported client brands.
238 + *
239 + * Brand names of unrelated AI vendors are deliberately not matched — only the
240 + * category terms and the clients Easy MCP AI itself integrates with.
241 + *
242 + * @param string $search Search query.
243 + *
244 + * @return bool True if the search query matches AI/MCP-related terms.
245 + */
246 + public static function matches_ai_search_keywords( $search ) {
247 + // Distinctive names match as substrings (for example, "claudecode").
248 + foreach ( array( 'model context protocol', 'claude', 'chatgpt' ) as $keyword ) {
249 + if ( false !== stripos( $search, $keyword ) ) {
250 + return true;
251 + }
252 + }
253 +
254 + // Short terms need boundaries (for example, not "email" or "fulfillment").
255 + return preg_match( '/(^|[^a-z0-9])(ai|mcp|llms?)([^a-z0-9]|$)/i', $search ) === 1;
256 + }
257 +
258 + /**
259 + * Extract the slugs from a list of plugin entries.
260 + *
261 + * @param array $plugins The plugins list.
262 + *
263 + * @return array
264 + */
265 + private function get_plugin_slugs( $plugins ) {
266 + $slugs = [];
267 + foreach ( $plugins as $plugin ) {
268 + $plugin = (array) $plugin;
269 + if ( isset( $plugin['slug'] ) ) {
270 + $slugs[] = $plugin['slug'];
271 + }
272 + }
273 +
274 + return $slugs;
275 + }
276 +
277 + /**
278 + * Remove the plugins matching the provided slugs from the list.
279 + *
280 + * @param array $plugins The plugins list.
281 + * @param array $slugs The slugs to remove.
282 + *
283 + * @return array
284 + */
285 + private function remove_plugins_by_slug( $plugins, $slugs ) {
286 + return array_values(
287 + array_filter(
288 + $plugins,
289 + function( $plugin ) use ( $slugs ) {
290 + $plugin = (array) $plugin;
291 +
292 + return ! isset( $plugin['slug'] ) || ! in_array( $plugin['slug'], $slugs, true );
293 + }
294 + )
295 + );
296 + }
297 +
298 + /**
299 + * Adjust the reported total results count after injecting plugins.
300 + *
301 + * @param object $res The result object.
302 + * @param int $delta The net number of plugins added to the list.
303 + *
304 + * @return object
305 + */
306 + private function adjust_results_count( $res, $delta ) {
307 + if ( 0 !== $delta && isset( $res->info['results'] ) && is_numeric( $res->info['results'] ) ) {
308 + $res->info['results'] = max( 0, (int) $res->info['results'] + $delta );
309 + }
310 +
102 311 return $res;
103 312 }
104 313
105 314 /**
315 + * Check if a plugin search query matches LMS-related terms.
316 + *
317 + * @param string $search Search query.
318 + *
319 + * @return bool True if the search query matches LMS-related terms.
320 + */
321 + public static function matches_lms_search_keywords( $search ) {
322 + $lms_keywords = array(
323 + 'lms',
324 + 'learn',
325 + 'course',
326 + 'courses',
327 + 'learning',
328 + 'academy',
329 + 'training',
330 + 'student',
331 + 'students',
332 + 'quiz',
333 + );
334 +
335 + foreach ( $lms_keywords as $keyword ) {
336 + if ( preg_match( '/(^|[^a-z0-9])' . preg_quote( $keyword, '/' ) . '([^a-z0-9]|$)/', $search ) === 1 ) {
337 + return true;
338 + }
339 + }
340 +
341 + return false;
342 + }
343 +
344 + /**
106 345 * Query plugins by author.
107 346 *
108 347 * @param object $args The arguments for the query.
109 348 *
@@ -115,16 +354,48 @@
115 354 $optimole_filter_slugs = apply_filters( 'themeisle_sdk_optimole_filter_slugs', [ 'optimole-wp' ] );
116 355 $filtered_from_optimole = $this->get_plugins_filtered_from_author( $args, $optimole_filter_slugs, 'Optimole' );
117 356 $featured = array_merge( $featured, $filtered_from_optimole );
118 357
119 - $themeisle_filter_slugs = apply_filters( 'themeisle_sdk_themeisle_filter_slugs', [ 'otter-blocks' ] );
358 + $themeisle_filter_slugs = apply_filters( 'themeisle_sdk_themeisle_filter_slugs', [ 'otter-blocks', 'wp-cloudflare-page-cache' ] );
120 359 $filtered_from_themeisle = $this->get_plugins_filtered_from_author( $args, $themeisle_filter_slugs );
121 360 $featured = array_merge( $featured, $filtered_from_themeisle );
122 361
123 - return $featured;
362 + // Easy MCP AI targets the Abilities API, available since WordPress 6.9.
363 + if ( version_compare( get_bloginfo( 'version' ), '6.9', '>=' ) ) {
364 + $easy_mcp_filter_slugs = apply_filters( 'themeisle_sdk_easy_mcp_filter_slugs', [ 'easy-mcp-ai' ] );
365 + $filtered_from_easy_mcp = $this->get_plugins_filtered_from_author( $args, $easy_mcp_filter_slugs, 'easymcpai' );
366 + $featured = array_merge( $featured, $filtered_from_easy_mcp );
367 + }
368 +
369 + return $this->dedupe_plugins_by_slug( $featured );
124 370 }
125 371
126 372 /**
373 + * Remove entries with a duplicate slug from a list of plugins, keeping the first occurrence.
374 + *
375 + * @param array $plugins The plugins list.
376 + *
377 + * @return array
378 + */
379 + private function dedupe_plugins_by_slug( $plugins ) {
380 + $seen = [];
381 + $deduped = [];
382 +
383 + foreach ( $plugins as $plugin ) {
384 + $array_plugin = (array) $plugin;
385 + if ( isset( $array_plugin['slug'] ) ) {
386 + if ( isset( $seen[ $array_plugin['slug'] ] ) ) {
387 + continue;
388 + }
389 + $seen[ $array_plugin['slug'] ] = true;
390 + }
391 + $deduped[] = $plugin;
392 + }
393 +
394 + return $deduped;
395 + }
396 +
397 + /**
127 398 * Get plugins filtered from an author.
128 399 *
129 400 * @param object $args The arguments for the query.
130 401 * @param array $filter_slugs The slugs to filter.
@@ -131,13 +402,13 @@
131 402 * @param string $author The author to filter.
132 403 *
133 404 * @return array
134 405 */
135 - private function get_plugins_filtered_from_author( $args, $filter_slugs = [], $author = 'Themeisle' ) {
406 + protected function get_plugins_filtered_from_author( $args, $filter_slugs = [], $author = 'Themeisle' ) {
136 407
137 408 $cached = get_transient( $this->transient_key . $author );
138 - if ( $cached ) {
139 - return $cached;
409 + if ( false !== $cached ) {
410 + return (array) $cached;
140 411 }
141 412
142 413 $new_args = [
143 414 'page' => 1,
@@ -148,17 +419,22 @@
148 419 ];
149 420
150 421 $api = plugins_api( 'query_plugins', $new_args );
151 422 if ( is_wp_error( $api ) ) {
423 + // Cache the failure for a shorter period to avoid hammering the API on every request.
424 + set_transient( $this->transient_key . $author, [], HOUR_IN_SECONDS );
425 +
152 426 return [];
153 427 }
154 428
155 - $filtered = array_filter(
156 - $api->plugins,
157 - function( $plugin ) use ( $filter_slugs ) {
158 - $array_plugin = (array) $plugin;
159 - return in_array( $array_plugin['slug'], $filter_slugs );
160 - }
429 + $filtered = array_values(
430 + array_filter(
431 + $api->plugins,
432 + function( $plugin ) use ( $filter_slugs ) {
433 + $array_plugin = (array) $plugin;
434 + return in_array( $array_plugin['slug'], $filter_slugs );
435 + }
436 + )
161 437 );
162 438
163 439 set_transient( $this->transient_key . $author, $filtered, 12 * HOUR_IN_SECONDS );
164 440