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 +212 -32 4.2.5 → 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,32 +182,167 @@
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 (
179 - strpos( $search, 'lms' ) !== false ||
180 - strpos( $search, 'learn' ) !== false
181 - ) {
182 - $filter_slugs = apply_filters( 'themeisle_sdk_masteriyo_filter_slugs', [ 'learning-management-system' ] );
183 - $masteriyo = $this->get_plugins_filtered_from_author( $args, $filter_slugs, 'masteriyo' );
186 + if ( ! self::matches_lms_search_keywords( $search ) ) {
187 + return $plugins;
188 + }
184 189
185 - if ( ! empty( $masteriyo ) ) {
186 - // Remove existing LMS plugin if present to avoid duplicates.
187 - $plugins = array_filter(
188 - $plugins,
189 - function( $plugin ) {
190 - return ( is_object( $plugin ) && isset( $plugin->slug ) && $plugin->slug !== 'learning-management-system' ) ||
191 - ( is_array( $plugin ) && isset( $plugin['slug'] ) && $plugin['slug'] !== 'learning-management-system' );
192 - }
193 - );
190 + $filter_slugs = apply_filters( 'themeisle_sdk_masteriyo_filter_slugs', [ 'learning-management-system' ] );
194 191
195 - $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;
196 251 }
197 252 }
198 - 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;
199 256 }
200 257
201 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 + /**
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 + /**
202 345 * Query plugins by author.
203 346 *
204 347 * @param object $args The arguments for the query.
205 348 *
@@ -215,12 +358,44 @@
215 358 $themeisle_filter_slugs = apply_filters( 'themeisle_sdk_themeisle_filter_slugs', [ 'otter-blocks', 'wp-cloudflare-page-cache' ] );
216 359 $filtered_from_themeisle = $this->get_plugins_filtered_from_author( $args, $themeisle_filter_slugs );
217 360 $featured = array_merge( $featured, $filtered_from_themeisle );
218 361
219 - 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 );
220 370 }
221 371
222 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 + /**
223 398 * Get plugins filtered from an author.
224 399 *
225 400 * @param object $args The arguments for the query.
226 401 * @param array $filter_slugs The slugs to filter.
@@ -230,10 +405,10 @@
230 405 */
231 406 protected function get_plugins_filtered_from_author( $args, $filter_slugs = [], $author = 'Themeisle' ) {
232 407
233 408 $cached = get_transient( $this->transient_key . $author );
234 - if ( $cached ) {
235 - return $cached;
409 + if ( false !== $cached ) {
410 + return (array) $cached;
236 411 }
237 412
238 413 $new_args = [
239 414 'page' => 1,
@@ -244,17 +419,22 @@
244 419 ];
245 420
246 421 $api = plugins_api( 'query_plugins', $new_args );
247 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 +
248 426 return [];
249 427 }
250 428
251 - $filtered = array_filter(
252 - $api->plugins,
253 - function( $plugin ) use ( $filter_slugs ) {
254 - $array_plugin = (array) $plugin;
255 - return in_array( $array_plugin['slug'], $filter_slugs );
256 - }
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 + )
257 437 );
258 438
259 439 set_transient( $this->transient_key . $author, $filtered, 12 * HOUR_IN_SECONDS );
260 440