PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.2.1
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.2.1
6.2.3 6.2.2 6.2.1 6.2 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1 6.0.12 6.0.11 4.6.0.1 5.0 5.0.1 5.0.2 5.0.3 5.0.3.1 5.0.3.2 5.1 5.1.1 5.2 All 156 releases
mainwp / class / class-mainwp-premium-update-registry.php

class-mainwp-premium-update-registry.php in MainWP Dashboard: Self-hosted WordPress Management for Agencies 6.2.1, at class/class-mainwp-premium-update-registry.php

613 lines 21.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MainWP Premium Update Registry.
4 *
5 * Built-in compatibility data for premium plugin & theme updates (MWP-1660).
6 *
7 * The registry is the default input of the public premium-update filters
8 * (mainwp_detect_premium_plugins_update, mainwp_request_update_premium_plugins,
9 * mainwp_detect_premium_themes_update, mainwp_request_update_premium_themes).
10 * Filter signatures and semantics are unchanged: filters receive flat arrays of
11 * exact identifiers, exactly as they always did. Prefix rules are matched
12 * internally (formalizing the historical inline 'yith-' substring check) and are
13 * not part of the filtered arrays.
14 *
15 * @package MainWP/Dashboard
16 */
17
18 namespace MainWP\Dashboard;
19
20 // Exit if accessed directly.
21 if ( ! defined( 'ABSPATH' ) ) {
22 exit;
23 }
24
25 /**
26 * Class MainWP_Premium_Update_Registry
27 *
28 * @package MainWP\Dashboard
29 */
30 class MainWP_Premium_Update_Registry { // phpcs:ignore Generic.Classes.OpeningBraceSameLine.ContentAfterBrace -- NOSONAR.
31
32 /**
33 * Option name for user-defined custom identifiers.
34 *
35 * Stored as an array of items: array( 'type' => 'plugin'|'theme', 'id' => string ).
36 * Custom entries always get both detect and request behavior.
37 *
38 * @var string
39 */
40 const OPTION_CUSTOM = 'mainwp_premium_updates_custom';
41
42 /**
43 * Get Class Name.
44 *
45 * @return string
46 */
47 public static function get_class_name() {
48 return __CLASS__;
49 }
50
51 /**
52 * Built-in registry entries.
53 *
54 * Fields per entry:
55 * - id : exact identifier (plugin basename or theme directory slug) or prefix.
56 * - name : display name used by the settings page when a site-reported name is unavailable.
57 * - type : 'plugin' | 'theme'.
58 * - match : 'exact' | 'prefix'.
59 * - detect : include in update-detection checks.
60 * - request : include in the premium request (wp-admin) update route.
61 *
62 * Identifiers are stored in canonical case; comparisons are case-insensitive.
63 *
64 * @return array[] Registry entries.
65 */
66 public static function get_entries() { // phpcs:ignore -- NOSONAR - long data method.
67 static $entries = null;
68 if ( null !== $entries ) {
69 return $entries;
70 }
71
72 $entries = array(
73 // Family rule: every YITH premium plugin uses the yith- folder prefix with an init.php
74 // main file. Replaces the historical inline strpos( 'yith-' ) detection hack and the
75 // single hardcoded YITH request entry.
76 array(
77 'id' => 'yith-',
78 'name' => 'YITH WooCommerce plugins',
79 'type' => 'plugin',
80 'match' => 'prefix',
81 'detect' => true,
82 'request' => true,
83 ),
84
85 // Elegant Themes family.
86 array(
87 'id' => 'Divi',
88 'name' => 'Divi',
89 'type' => 'theme',
90 'match' => 'exact',
91 'detect' => true,
92 'request' => true,
93 ),
94 array(
95 'id' => 'Extra',
96 'name' => 'Extra',
97 'type' => 'theme',
98 'match' => 'exact',
99 'detect' => true,
100 'request' => true,
101 ),
102 array(
103 'id' => 'divi-builder/divi-builder.php',
104 'name' => 'Divi Builder',
105 'type' => 'plugin',
106 'match' => 'exact',
107 'detect' => true,
108 'request' => true,
109 ),
110 array(
111 'id' => 'bloom/bloom.php',
112 'name' => 'Bloom',
113 'type' => 'plugin',
114 'match' => 'exact',
115 'detect' => true,
116 'request' => true,
117 ),
118
119 array(
120 'id' => 'yootheme',
121 'name' => 'YOOtheme Pro',
122 'type' => 'theme',
123 'match' => 'exact',
124 'detect' => true,
125 'request' => true,
126 ),
127 array(
128 'id' => 'bricks',
129 'name' => 'Bricks',
130 'type' => 'theme',
131 'match' => 'exact',
132 'detect' => true,
133 'request' => true,
134 ),
135 array(
136 'id' => 'Avada',
137 'name' => 'Avada',
138 'type' => 'theme',
139 'match' => 'exact',
140 'detect' => true,
141 'request' => false,
142 ),
143
144 array(
145 'id' => 'js_composer/js_composer.php',
146 'name' => 'WPBakery Page Builder',
147 'type' => 'plugin',
148 'match' => 'exact',
149 'detect' => true,
150 'request' => true,
151 ),
152 array(
153 'id' => 'bb-theme-builder/bb-theme-builder.php',
154 'name' => 'Beaver Themer',
155 'type' => 'plugin',
156 'match' => 'exact',
157 'detect' => true,
158 'request' => false,
159 ),
160 // WooPack (IdeaBox): folder 'woopack' is vendor-confirmed, but the main file name is
161 // not independently verified (IdeaBox has folder/file asymmetry precedent), so no
162 // entry ships. Add one via the registry intake once a verified identifier surfaces.
163 // See MWP-1660.
164
165 // Oxygen. Classic (<= 4.x) and Oxygen 6 share the 'oxygen' folder but use different
166 // main files, so a site reports exactly one of the two basenames.
167 array(
168 'id' => 'oxygen/functions.php',
169 'name' => 'Oxygen',
170 'type' => 'plugin',
171 'match' => 'exact',
172 'detect' => true,
173 'request' => true,
174 ),
175 // Oxygen 6 main file, verified by probing the vendor's own production site. See MWP-1660.
176 array(
177 'id' => 'oxygen/plugin.php',
178 'name' => 'Oxygen 6',
179 'type' => 'plugin',
180 'match' => 'exact',
181 'detect' => true,
182 'request' => true,
183 ),
184
185 // Carried over from the pre-registry hardcoded detection list (detect-only).
186 array(
187 'id' => 'ithemes-security-pro/ithemes-security-pro.php',
188 'name' => 'Solid Security Pro',
189 'type' => 'plugin',
190 'match' => 'exact',
191 'detect' => true,
192 'request' => false,
193 ),
194 array(
195 'id' => 'monarch/monarch.php',
196 'name' => 'Monarch',
197 'type' => 'plugin',
198 'match' => 'exact',
199 'detect' => true,
200 'request' => false,
201 ),
202 array(
203 'id' => 'cornerstone/cornerstone.php',
204 'name' => 'Cornerstone',
205 'type' => 'plugin',
206 'match' => 'exact',
207 'detect' => true,
208 'request' => false,
209 ),
210 // UpdraftPlus Premium ships the identical basename as the free version; the entry is
211 // intentionally kept even though it also matches free installs.
212 array(
213 'id' => 'updraftplus/updraftplus.php',
214 'name' => 'UpdraftPlus Premium',
215 'type' => 'plugin',
216 'match' => 'exact',
217 'detect' => true,
218 'request' => false,
219 ),
220 array(
221 'id' => 'wp-all-import-pro/wp-all-import-pro.php',
222 'name' => 'WP All Import Pro',
223 'type' => 'plugin',
224 'match' => 'exact',
225 'detect' => true,
226 'request' => false,
227 ),
228 array(
229 'id' => 'bbq-pro/bbq-pro.php',
230 'name' => 'BBQ Pro',
231 'type' => 'plugin',
232 'match' => 'exact',
233 'detect' => true,
234 'request' => false,
235 ),
236 // Audit candidate: SeedProd renamed its pro plugin in later versions. See MWP-1660.
237 array(
238 'id' => 'seedprod-coming-soon-pro-5/seedprod-coming-soon-pro-5.php',
239 'name' => 'SeedProd Pro',
240 'type' => 'plugin',
241 'match' => 'exact',
242 'detect' => true,
243 'request' => false,
244 ),
245 array(
246 'id' => 'elementor-pro/elementor-pro.php',
247 'name' => 'Elementor Pro',
248 'type' => 'plugin',
249 'match' => 'exact',
250 'detect' => true,
251 'request' => false,
252 ),
253 array(
254 'id' => 'bbpowerpack/bb-powerpack.php',
255 'name' => 'PowerPack for Beaver Builder',
256 'type' => 'plugin',
257 'match' => 'exact',
258 'detect' => true,
259 'request' => false,
260 ),
261 array(
262 'id' => 'bb-ultimate-addon/bb-ultimate-addon.php',
263 'name' => 'Ultimate Addons for Beaver Builder',
264 'type' => 'plugin',
265 'match' => 'exact',
266 'detect' => true,
267 'request' => false,
268 ),
269 // Audit candidate: WebARX rebranded to Patchstack; the folder likely changed. See MWP-1660.
270 array(
271 'id' => 'webarx/webarx.php',
272 'name' => 'WebARX',
273 'type' => 'plugin',
274 'match' => 'exact',
275 'detect' => true,
276 'request' => false,
277 ),
278 // Audit candidate. See MWP-1660.
279 array(
280 'id' => 'leco-client-portal/leco-client-portal.php',
281 'name' => 'LECO Client Portal',
282 'type' => 'plugin',
283 'match' => 'exact',
284 'detect' => true,
285 'request' => false,
286 ),
287 // Note: elementor-extras (discontinued 2021) was removed from the carried-over list.
288 array(
289 'id' => 'wp-schema-pro/wp-schema-pro.php',
290 'name' => 'Schema Pro',
291 'type' => 'plugin',
292 'match' => 'exact',
293 'detect' => true,
294 'request' => false,
295 ),
296 array(
297 'id' => 'convertpro/convertpro.php',
298 'name' => 'Convert Pro',
299 'type' => 'plugin',
300 'match' => 'exact',
301 'detect' => true,
302 'request' => false,
303 ),
304 array(
305 'id' => 'convertpro-addon/convertpro-addon.php',
306 'name' => 'Convert Pro Addon',
307 'type' => 'plugin',
308 'match' => 'exact',
309 'detect' => true,
310 'request' => false,
311 ),
312 array(
313 'id' => 'astra-addon/astra-addon.php',
314 'name' => 'Astra Pro',
315 'type' => 'plugin',
316 'match' => 'exact',
317 'detect' => true,
318 'request' => false,
319 ),
320 array(
321 'id' => 'astra-portfolio/astra-portfolio.php',
322 'name' => 'Astra Portfolio',
323 'type' => 'plugin',
324 'match' => 'exact',
325 'detect' => true,
326 'request' => false,
327 ),
328 array(
329 'id' => 'astra-pro-sites/astra-pro-sites.php',
330 'name' => 'Astra Premium Starter Templates',
331 'type' => 'plugin',
332 'match' => 'exact',
333 'detect' => true,
334 'request' => false,
335 ),
336 // Audit candidate: Smash Balloon renamed products over time. See MWP-1660.
337 array(
338 'id' => 'custom-facebook-feed-pro/custom-facebook-feed.php',
339 'name' => 'Custom Facebook Feed Pro',
340 'type' => 'plugin',
341 'match' => 'exact',
342 'detect' => true,
343 'request' => false,
344 ),
345 array(
346 'id' => 'ultimate-elementor/ultimate-elementor.php',
347 'name' => 'Ultimate Addons for Elementor',
348 'type' => 'plugin',
349 'match' => 'exact',
350 'detect' => true,
351 'request' => false,
352 ),
353 array(
354 'id' => 'gp-premium/gp-premium.php',
355 'name' => 'GP Premium',
356 'type' => 'plugin',
357 'match' => 'exact',
358 'detect' => true,
359 'request' => false,
360 ),
361 array(
362 'id' => 'flying-press/flying-press.php',
363 'name' => 'FlyingPress',
364 'type' => 'plugin',
365 'match' => 'exact',
366 'detect' => true,
367 'request' => false,
368 ),
369 array(
370 'id' => 'wp-rocket/wp-rocket.php',
371 'name' => 'WP Rocket',
372 'type' => 'plugin',
373 'match' => 'exact',
374 'detect' => true,
375 'request' => false,
376 ),
377 array(
378 'id' => 'fluentformpro/fluentformpro.php',
379 'name' => 'Fluent Forms Pro',
380 'type' => 'plugin',
381 'match' => 'exact',
382 'detect' => true,
383 'request' => false,
384 ),
385 array(
386 'id' => 'fluentform-signature/fluentform-signature.php',
387 'name' => 'Fluent Forms Signature',
388 'type' => 'plugin',
389 'match' => 'exact',
390 'detect' => true,
391 'request' => false,
392 ),
393 array(
394 'id' => 'fluentcampaign-pro/fluentcampaign-pro.php',
395 'name' => 'FluentCRM Pro',
396 'type' => 'plugin',
397 'match' => 'exact',
398 'detect' => true,
399 'request' => false,
400 ),
401 array(
402 'id' => 'fluent-support-pro/fluent-support-pro.php',
403 'name' => 'Fluent Support Pro',
404 'type' => 'plugin',
405 'match' => 'exact',
406 'detect' => true,
407 'request' => false,
408 ),
409 array(
410 'id' => 'ninja-tables-pro/ninja-tables-pro.php',
411 'name' => 'Ninja Tables Pro',
412 'type' => 'plugin',
413 'match' => 'exact',
414 'detect' => true,
415 'request' => false,
416 ),
417 array(
418 'id' => 'fluent-booking-pro/fluent-booking-pro.php',
419 'name' => 'FluentBooking Pro',
420 'type' => 'plugin',
421 'match' => 'exact',
422 'detect' => true,
423 'request' => false,
424 ),
425 array(
426 'id' => 'wp-social-ninja-pro/wp-social-ninja-pro.php',
427 'name' => 'WP Social Ninja Pro',
428 'type' => 'plugin',
429 'match' => 'exact',
430 'detect' => true,
431 'request' => false,
432 ),
433 array(
434 'id' => 'wp-payment-form-pro/wp-payment-form-pro.php',
435 'name' => 'Paymattic Pro',
436 'type' => 'plugin',
437 'match' => 'exact',
438 'detect' => true,
439 'request' => false,
440 ),
441 );
442
443 return $entries;
444 }
445
446 /**
447 * Read the canonical identifier from a list or keyed synced inventory.
448 *
449 * @param int|string $inventory_key Inventory array key.
450 * @param mixed $info Installed product data.
451 * @return string Valid identifier, or an empty string for malformed data.
452 */
453 public static function get_inventory_identifier( $inventory_key, $info ) {
454 if ( ! is_array( $info ) ) {
455 return '';
456 }
457 $identifier = '';
458 if ( is_string( $inventory_key ) && '' !== $inventory_key ) {
459 $identifier = $inventory_key;
460 } elseif ( isset( $info['slug'] ) && is_string( $info['slug'] ) ) {
461 $identifier = $info['slug'];
462 }
463 $identifier = trim( $identifier );
464 if ( '' === $identifier || ! preg_match( '/^[A-Za-z0-9\-_.\/]+$/', $identifier ) ) {
465 return '';
466 }
467 return $identifier;
468 }
469
470 /**
471 * User-defined custom identifiers (settings page).
472 *
473 * @return array[] Items: array( 'type' => 'plugin'|'theme', 'id' => string ).
474 */
475 public static function get_custom_entries() {
476 $custom = get_option( static::OPTION_CUSTOM, array() );
477 if ( ! is_array( $custom ) ) {
478 return array();
479 }
480 $items = array();
481 foreach ( $custom as $item ) {
482 if ( ! is_array( $item ) || empty( $item['id'] ) || empty( $item['type'] ) ) {
483 continue;
484 }
485 $type = 'theme' === $item['type'] ? 'theme' : 'plugin';
486 $id = sanitize_text_field( $item['id'] );
487 if ( '' === $id ) {
488 continue;
489 }
490 $items[] = array(
491 'type' => $type,
492 'id' => $id,
493 );
494 }
495 return $items;
496 }
497
498 /**
499 * Save custom identifiers.
500 *
501 * @param array $items Items: array( 'type' => 'plugin'|'theme', 'id' => string ).
502 */
503 public static function save_custom_entries( $items ) {
504 $clean = array();
505 $seen = array();
506 if ( is_array( $items ) ) {
507 foreach ( $items as $item ) {
508 if ( ! is_array( $item ) || empty( $item['id'] ) ) {
509 continue;
510 }
511 $type = isset( $item['type'] ) && 'theme' === $item['type'] ? 'theme' : 'plugin';
512 $id = sanitize_text_field( wp_unslash( $item['id'] ) );
513 $id = trim( $id );
514 if ( '' === $id || ! preg_match( '/^[A-Za-z0-9\-_.\/]+$/', $id ) ) {
515 continue;
516 }
517 $key = $type . '|' . strtolower( $id );
518 if ( isset( $seen[ $key ] ) ) {
519 continue;
520 }
521 $seen[ $key ] = true;
522 $clean[] = array(
523 'type' => $type,
524 'id' => $id,
525 );
526 }
527 }
528 MainWP_Utility::update_option( static::OPTION_CUSTOM, $clean );
529 }
530
531 /**
532 * Default exact identifiers for one of the four public filters.
533 *
534 * Prefix rules are not included: filters keep their historical semantics of
535 * flat exact-identifier arrays. Custom entries are included (both flags).
536 *
537 * @param string $type 'plugin' | 'theme'.
538 * @param string $purpose 'detect' | 'request'.
539 *
540 * @return string[] Exact identifiers.
541 */
542 public static function get_filter_defaults( $type, $purpose ) {
543 $ids = array();
544 foreach ( static::get_entries() as $entry ) {
545 if ( $entry['type'] !== $type || 'exact' !== $entry['match'] ) {
546 continue;
547 }
548 if ( empty( $entry[ 'request' === $purpose ? 'request' : 'detect' ] ) ) {
549 continue;
550 }
551 $ids[] = $entry['id'];
552 }
553 foreach ( static::get_custom_entries() as $item ) {
554 if ( $item['type'] === $type ) {
555 $ids[] = $item['id'];
556 }
557 }
558 return array_values( array_unique( $ids ) );
559 }
560
561 /**
562 * Prefix rules for internal matching.
563 *
564 * @param string $type 'plugin' | 'theme'.
565 * @param string $purpose 'detect' | 'request'.
566 *
567 * @return string[] Prefixes.
568 */
569 public static function get_prefixes( $type, $purpose ) {
570 $prefixes = array();
571 foreach ( static::get_entries() as $entry ) {
572 if ( $entry['type'] !== $type || 'prefix' !== $entry['match'] ) {
573 continue;
574 }
575 if ( empty( $entry[ 'request' === $purpose ? 'request' : 'detect' ] ) ) {
576 continue;
577 }
578 $prefixes[] = $entry['id'];
579 }
580 return $prefixes;
581 }
582
583 /**
584 * Case-insensitive identifier matching against exact identifiers and prefixes.
585 *
586 * Registry identifiers are never sent to child sites; matching only decides
587 * whether existing premium-update behavior applies to a child-reported slug.
588 *
589 * @param string $slug Child-reported identifier.
590 * @param string[] $ids Exact identifiers (post-filter).
591 * @param string[] $prefixes Prefix rules.
592 *
593 * @return bool
594 */
595 public static function slug_matches( $slug, $ids, $prefixes ) {
596 if ( ! is_string( $slug ) || '' === $slug ) {
597 return false;
598 }
599 foreach ( (array) $ids as $id ) {
600 if ( is_string( $id ) && 0 === strcasecmp( $slug, $id ) ) {
601 return true;
602 }
603 }
604 foreach ( (array) $prefixes as $prefix ) {
605 if ( is_string( $prefix ) && '' !== $prefix && 0 === strncasecmp( $slug, $prefix, strlen( $prefix ) ) ) {
606 return true;
607 }
608 }
609 return false;
610 }
611
612 }
613