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/Featured_plugins.php +183 -30 4.2.8 → trunk 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