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 +183 -30 4.0.7 → 4.0.9 View file →
@@ -148,23 +148,31 @@
148 148 return $res;
149 149 }
150 150
151 151 if ( isset( $args->page ) && 1 === (int) $args->page && isset( $args->search ) && ! empty( $args->search ) ) {
152 - $res->plugins = $this->maybe_prepend_lms_plugin( $res->plugins, $args );
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 +
159 + if ( ! isset( $args->browse ) || $args->browse !== 'featured' ) {
153 160 return $res;
154 161 }
155 162
156 - if ( ! isset( $args->browse ) || $args->browse !== 'featured' ) {
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 ) {
157 165 return $res;
158 166 }
159 167
160 168 $featured = $this->query_plugins_by_author( $args );
161 169
162 - $plugins = array_merge( $featured, (array) $res->plugins );
163 - $plugins = array_slice( $plugins, 0, $res->info['results'] );
164 - $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 );
165 173
166 - return $res;
174 + return $this->adjust_results_count( $res, count( $res->plugins ) - $original_count );
167 175 }
168 176
169 177 /**
170 178 * Prepend the LMS plugin if the search query matches LMS-related terms.
@@ -174,29 +182,137 @@
174 182 * @return array
175 183 */
176 184 private function maybe_prepend_lms_plugin( $plugins, $args ) {
177 185 $search = isset( $args->search ) ? strtolower( $args->search ) : '';
178 - if ( $this->matches_lms_search_keywords( $search ) ) {
179 - $filter_slugs = apply_filters( 'themeisle_sdk_masteriyo_filter_slugs', [ 'learning-management-system' ] );
180 - $masteriyo = $this->get_plugins_filtered_from_author( $args, $filter_slugs, 'masteriyo' );
186 + if ( ! self::matches_lms_search_keywords( $search ) ) {
187 + return $plugins;
188 + }
181 189
182 - if ( ! empty( $masteriyo ) ) {
183 - // Remove existing LMS plugin if present to avoid duplicates.
184 - $plugins = array_filter(
185 - $plugins,
186 - function( $plugin ) {
187 - return ( is_object( $plugin ) && isset( $plugin->slug ) && $plugin->slug !== 'learning-management-system' ) ||
188 - ( is_array( $plugin ) && isset( $plugin['slug'] ) && $plugin['slug'] !== 'learning-management-system' );
189 - }
190 - );
190 + $filter_slugs = apply_filters( 'themeisle_sdk_masteriyo_filter_slugs', [ 'learning-management-system' ] );
191 191
192 - $plugins = array_merge( $masteriyo, $plugins );
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;
193 251 }
194 252 }
195 - return $plugins;
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;
196 256 }
197 257
198 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 +
311 + return $res;
312 + }
313 +
314 + /**
199 315 * Check if a plugin search query matches LMS-related terms.
200 316 *
201 317 * @param string $search Search query.
202 318 *
@@ -201,9 +317,9 @@
201 317 * @param string $search Search query.
202 318 *
203 319 * @return bool True if the search query matches LMS-related terms.
204 320 */
205 - private function matches_lms_search_keywords( $search ) {
321 + public static function matches_lms_search_keywords( $search ) {
206 322 $lms_keywords = array(
207 323 'lms',
208 324 'learn',
209 325 'course',
@@ -242,12 +358,44 @@
242 358 $themeisle_filter_slugs = apply_filters( 'themeisle_sdk_themeisle_filter_slugs', [ 'otter-blocks', 'wp-cloudflare-page-cache' ] );
243 359 $filtered_from_themeisle = $this->get_plugins_filtered_from_author( $args, $themeisle_filter_slugs );
244 360 $featured = array_merge( $featured, $filtered_from_themeisle );
245 361
246 - 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 );
247 370 }
248 371
249 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 + /**
250 398 * Get plugins filtered from an author.
251 399 *
252 400 * @param object $args The arguments for the query.
253 401 * @param array $filter_slugs The slugs to filter.
@@ -257,10 +405,10 @@
257 405 */
258 406 protected function get_plugins_filtered_from_author( $args, $filter_slugs = [], $author = 'Themeisle' ) {
259 407
260 408 $cached = get_transient( $this->transient_key . $author );
261 - if ( $cached ) {
262 - return $cached;
409 + if ( false !== $cached ) {
410 + return (array) $cached;
263 411 }
264 412
265 413 $new_args = [
266 414 'page' => 1,
@@ -271,17 +419,22 @@
271 419 ];
272 420
273 421 $api = plugins_api( 'query_plugins', $new_args );
274 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 +
275 426 return [];
276 427 }
277 428
278 - $filtered = array_filter(
279 - $api->plugins,
280 - function( $plugin ) use ( $filter_slugs ) {
281 - $array_plugin = (array) $plugin;
282 - return in_array( $array_plugin['slug'], $filter_slugs );
283 - }
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 + )
284 437 );
285 438
286 439 set_transient( $this->transient_key . $author, $filtered, 12 * HOUR_IN_SECONDS );
287 440