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
optimole-wp / vendor / codeinwp / themeisle-sdk / src / Modules / Featured_plugins.php

Featured_plugins.php in Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization trunk, at vendor/codeinwp/themeisle-sdk/src/Modules/Featured_plugins.php

444 lines 12.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * File responsible for showing plugins inside the featured tab.
4 *
5 * This is used to display information about limited events, such as Black Friday.
6 *
7 * @package ThemeIsleSDK
8 * @subpackage Modules
9 * @copyright Copyright (c) 2017, Marius Cristea
10 * @license http://opensource.org/licenses/gpl-3.0.php GNU Public License
11 * @since 3.3.0
12 */
13 namespace ThemeisleSDK\Modules;
14
15 use ThemeisleSDK\Common\Abstract_Module;
16 use ThemeisleSDK\Loader;
17 use ThemeisleSDK\Product;
18
19 // Exit if accessed directly.
20 if ( ! defined( 'ABSPATH' ) ) {
21 exit;
22 }
23
24 /**
25 * Featured_Plugins module for the ThemeIsle SDK.
26 */
27 class Featured_Plugins extends Abstract_Module {
28
29 /**
30 * The transient key prefix.
31 *
32 * @var string $transient_key
33 */
34 private $transient_key = 'themeisle_sdk_featured_plugins_';
35
36 /**
37 * The current product instance.
38 *
39 * @var Product|null
40 */
41 protected $product = null;
42
43 /**
44 * Check if the module can be loaded.
45 *
46 * @param Product $product Product data.
47 *
48 * @return bool
49 */
50 public function can_load( $product ) {
51 if ( $this->is_from_partner( $product ) ) {
52 return false;
53 }
54
55 if ( $product->is_wordpress_available() ) {
56 return false;
57 }
58
59 return ! apply_filters( 'themeisle_sdk_disable_featured_plugins', false );
60 }
61
62 /**
63 * Load the module for the selected product.
64 *
65 * @param Product $product Product data.
66 *
67 * @return void
68 */
69 public function load( $product ) {
70 $this->product = $product;
71
72 if ( ! current_user_can( 'install_plugins' ) ) {
73 return;
74 }
75
76 // bail if we already registered a filter for the plugin API.
77 if ( apply_filters( 'themeisle_sdk_plugin_api_filter_registered', false ) ) {
78 return;
79 }
80 add_filter( 'themeisle_sdk_plugin_api_filter_registered', '__return_true' );
81
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' ] );
86 }
87
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 /**
137 * Filter the plugin API results to include the featured plugins.
138 *
139 * @param object $res The result object.
140 * @param string $action The type of information being requested from the Plugin Install API.
141 * @param object $args Plugin API arguments.
142 *
143 * @return object
144 */
145 public function filter_plugin_api_results( $res, $action, $args ) {
146
147 if ( 'query_plugins' !== $action ) {
148 return $res;
149 }
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
159 if ( ! isset( $args->browse ) || $args->browse !== 'featured' ) {
160 return $res;
161 }
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
168 $featured = $this->query_plugins_by_author( $args );
169
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 );
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
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 /**
345 * Query plugins by author.
346 *
347 * @param object $args The arguments for the query.
348 *
349 * @return array
350 */
351 private function query_plugins_by_author( $args ) {
352 $featured = [];
353
354 $optimole_filter_slugs = apply_filters( 'themeisle_sdk_optimole_filter_slugs', [ 'optimole-wp' ] );
355 $filtered_from_optimole = $this->get_plugins_filtered_from_author( $args, $optimole_filter_slugs, 'Optimole' );
356 $featured = array_merge( $featured, $filtered_from_optimole );
357
358 $themeisle_filter_slugs = apply_filters( 'themeisle_sdk_themeisle_filter_slugs', [ 'otter-blocks', 'wp-cloudflare-page-cache' ] );
359 $filtered_from_themeisle = $this->get_plugins_filtered_from_author( $args, $themeisle_filter_slugs );
360 $featured = array_merge( $featured, $filtered_from_themeisle );
361
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 );
370 }
371
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 /**
398 * Get plugins filtered from an author.
399 *
400 * @param object $args The arguments for the query.
401 * @param array $filter_slugs The slugs to filter.
402 * @param string $author The author to filter.
403 *
404 * @return array
405 */
406 protected function get_plugins_filtered_from_author( $args, $filter_slugs = [], $author = 'Themeisle' ) {
407
408 $cached = get_transient( $this->transient_key . $author );
409 if ( false !== $cached ) {
410 return (array) $cached;
411 }
412
413 $new_args = [
414 'page' => 1,
415 'per_page' => 36,
416 'locale' => get_user_locale(),
417 'author' => $author,
418 'wp_version' => isset( $args->wp_version ) ? $args->wp_version : get_bloginfo( 'version' ),
419 ];
420
421 $api = plugins_api( 'query_plugins', $new_args );
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
426 return [];
427 }
428
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 )
437 );
438
439 set_transient( $this->transient_key . $author, $filtered, 12 * HOUR_IN_SECONDS );
440
441 return $filtered;
442 }
443 }
444