PluginProbe
Plugin Groups / trunk
Plugin Groups vtrunk
trunk 1.0.3 1.1.0 1.2.1 1.2.2 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.9 3.0.0
← All changes | classes/class-plugin-groups.php +508 -134 2.0.6trunk View file →
@@ -16,9 +16,9 @@
16 16
17 17 /**
18 18 * The single instance of the class.
19 19 *
20 - * @var Plugin_Groups
20 + * @var groups\classes\Plugin_Groups
21 21 */
22 22 protected static $instance = null;
23 23
24 24 /**
@@ -39,9 +39,9 @@
39 39 * Holds the plugin config.
40 40 *
41 41 * @var array
42 42 */
43 - protected $config = array();
43 + protected $config = [];
44 44
45 45 /**
46 46 * Holds the Groups.
47 47 *
@@ -46,9 +46,9 @@
46 46 * Holds the Groups.
47 47 *
48 48 * @var array
49 49 */
50 - protected $groups = array();
50 + protected $groups = [];
51 51
52 52 /**
53 53 * Holds the menu slug.
54 54 *
@@ -74,9 +74,9 @@
74 74 * Holds a list of missing plugins.
75 75 *
76 76 * @var array
77 77 */
78 - protected $missing = array();
78 + protected $missing = [];
79 79
80 80 /**
81 81 * Holds the current group and status path.
82 82 *
@@ -84,8 +84,16 @@
84 84 */
85 85 protected $current_nav_path;
86 86
87 87 /**
88 + * Development mode flag.
89 + *
90 + * @since 3.0.0
91 + * @var bool|string
92 + */
93 + private bool|string $dev_mode = false;
94 +
95 + /**
88 96 * Hold the record of the plugins current version for upgrade.
89 97 *
90 98 * @var string
91 99 */
@@ -107,10 +115,12 @@
107 115 $plugin = get_plugin_data( PLGGRP_CORE );
108 116 $this->plugin_name = $plugin['Name'];
109 117 $this->version = $plugin['Version'];
110 118
111 - spl_autoload_register( array( $this, 'autoload_class' ), true, false );
119 + spl_autoload_register( [ $this, 'autoload_class' ], true, false );
112 120
121 + $this->dev_mode = defined( 'PLGGRP_DEV_MODE' ) ? PLGGRP_DEV_MODE : false;
122 +
113 123 // Start hooks.
114 124 $this->setup_hooks();
115 125
116 126 // Init the bulk actions.
@@ -124,23 +134,25 @@
124 134 */
125 135 protected function setup_hooks() {
126 136
127 137 // Load plugin text domain
128 - add_action( 'init', array( $this, 'plugin_groups_init' ), PHP_INT_MAX ); // Always the last thing to init.
129 - add_action( 'admin_init', array( $this, 'admin_init' ) );
130 - add_action( 'admin_menu', array( $this, 'admin_menu' ) );
131 - add_action( 'network_admin_menu', array( $this, 'admin_menu' ) );
132 - add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
133 - add_filter( 'views_plugins', array( $this, 'add_groups' ), PHP_INT_MAX );
134 - add_filter( 'views_plugins-network', array( $this, 'add_groups' ), PHP_INT_MAX );
135 - add_filter( 'all_plugins', array( $this, 'catch_selected_group' ) );
136 - add_filter( 'show_advanced_plugins', array( $this, 'filter_shown_status' ), 10, 2 );
137 - add_filter( 'site_transient_update_plugins', array( $this, 'alter_update_plugins' ) );
138 - add_action( 'pre_current_active_plugins', array( $this, 'render_group_navigation' ) );
139 - add_filter( 'bulk_actions-plugins', array( $this, 'bulk_actions' ) );
140 - add_action( 'admin_bar_menu', array( $this, 'admin_bar_item' ), 100 );
141 - add_filter( 'self_admin_url', array( $this, 'append_group_to_self' ), 10, 3 );
142 - add_filter( 'plugin_action_links', array( $this, 'append_group_to_actions' ) );
138 + add_action( 'init', [ $this, 'plugin_groups_init' ], PHP_INT_MAX ); // Always the last thing to init.
139 + add_action( 'admin_init', [ $this, 'admin_init' ] );
140 + add_action( 'admin_menu', [ $this, 'admin_menu' ] );
141 + add_action( 'network_admin_menu', [ $this, 'admin_menu' ] );
142 + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
143 + add_filter( 'views_plugins', [ $this, 'add_groups' ], PHP_INT_MAX );
144 + add_filter( 'views_plugins-network', [ $this, 'add_groups' ], PHP_INT_MAX );
145 + add_filter( 'all_plugins', [ $this, 'catch_selected_group' ] );
146 + add_filter( 'show_advanced_plugins', [ $this, 'filter_shown_status' ], 10, 2 );
147 + add_filter( 'site_transient_update_plugins', [ $this, 'alter_update_plugins' ] );
148 + add_action( 'pre_current_active_plugins', [ $this, 'render_group_navigation' ] );
149 + add_filter( 'bulk_actions-plugins', [ $this, 'bulk_actions' ] );
150 + add_action( 'admin_bar_menu', [ $this, 'admin_bar_item' ], 100 );
151 + add_filter( 'self_admin_url', [ $this, 'append_group_to_self' ], 10, 3 );
152 + add_filter( 'plugin_action_links', [ $this, 'append_group_to_actions' ] );
153 + add_action( 'activated_plugin', [ $this, 'auto_assign_by_keywords' ], 10, 0 );
154 + add_action( 'upgrader_process_complete', [ $this, 'after_plugin_upgrade' ], 10, 2 );
143 155 }
144 156
145 157 /**
146 158 * Append the current group id to plugin action links to maintain selection on activation and deactivation of plugins.
@@ -222,12 +234,12 @@
222 234 public function filter_shown_status( $show, $status ) {
223 235
224 236 global $plugins;
225 237
226 - static $removes = array(
238 + static $removes = [
227 239 'mustuse',
228 240 'dropins',
229 - );
241 + ];
230 242 if ( $this->current_group && in_array( $status, $removes, true ) ) {
231 243 $show = false;
232 244 }
233 245 if ( true === $this->config['params']['legacyGrouping'] ) {
@@ -243,9 +255,9 @@
243 255 public function render_group_navigation() {
244 256
245 257 // Use new group system.
246 258 if ( true !== $this->config['params']['legacyGrouping'] && 'groups-dropdown' !== $this->config['params']['navStyle'] ) {
247 - $parts = array();
259 + $parts = [];
248 260 $parts[] = $this->make_all_tag();
249 261 foreach ( $this->groups as $key => $plugins ) {
250 262 $parts[] = $this->make_group_tag( $key );
251 263 }
@@ -250,12 +262,13 @@
250 262 $parts[] = $this->make_group_tag( $key );
251 263 }
252 264
253 265 $groups = implode( "", $parts );
254 - $group_set = Utils::build_tag( 'ul', array( 'class' => array( $this->config['params']['navStyle'] ) ), $groups );
255 - $html = Utils::build_tag( 'div', array( 'class' => self::$slug ), $group_set );
266 + $group_set = Utils::build_tag( 'ul', [ 'class' => [ $this->config['params']['navStyle'] ] ], $groups );
267 + $html = Utils::build_tag( 'div', [ 'class' => self::$slug ], $group_set );
256 268 if ( 1 < count( $parts ) ) {
257 269 echo wp_kses( $html, wp_kses_allowed_html( 'post' ) );
270 + $this->render_group_bulk_actions();
258 271 }
259 272 }
260 273 }
261 274
@@ -283,9 +296,9 @@
283 296 * @return void|string
284 297 */
285 298 public function dropdown_navigation( $echo = true ) {
286 299
287 - $html = array();
300 + $html = [];
288 301 $html[] = '<label for="bulk-action-selector-groups" class="screen-reader-text">' . __( 'All groups', self::$slug ) . '</label>';
289 302 $html[] = '<select id="bulk-action-selector-groups" onChange="window.location = this.value">';
290 303 $html[] = '<option value="' . esc_url( $this->get_nav_url() ) . '" ' . ( ! empty( $this->current_group ) ? 'selected="selected"' : '' ) . '>' . __( 'All groups', self::$slug ) . '</option>';
291 304
@@ -295,9 +308,9 @@
295 308 if ( $this->current_group === $key ) {
296 309 $selected = ' selected="selected"';
297 310 }
298 311
299 - $html[] = "\t" . '<option value="' . esc_url( $this->get_nav_url( $key ) ) . '"' . $selected . '>' . $group['name'] . ' (' . count( $plugins ) . ')</option>' . "\n";
312 + $html[] = "\t" . '<option value="' . esc_url( $this->get_nav_url( $key ) ) . '"' . $selected . '>' . esc_html( $group['name'] ) . ' (' . count( $plugins ) . ')</option>' . "\n";
300 313 }
301 314
302 315 $html[] = "</select>\n";
303 316 $html = implode( $html );
@@ -303,12 +316,158 @@
303 316 $html = implode( $html );
304 317 if ( false === $echo ) {
305 318 return $html;
306 319 }
307 - echo $html;
320 + echo wp_kses(
321 + $html,
322 + [
323 + 'label' => [
324 + 'for' => true,
325 + 'class' => true,
326 + ],
327 + 'select' => [
328 + 'id' => true,
329 + 'onchange' => true,
330 + ],
331 + 'option' => [
332 + 'value' => true,
333 + 'selected' => true,
334 + ],
335 + ]
336 + );
337 + $this->render_group_bulk_actions();
308 338 }
309 339
310 340 /**
341 + * Render bulk activate/deactivate/update controls for the currently selected group.
342 + */
343 + protected function render_group_bulk_actions() {
344 +
345 + if ( empty( $this->config['params']['groupBulkActions'] ) || true === $this->config['params']['legacyGrouping'] || empty( $this->current_group ) || empty( $this->groups[ $this->current_group ] ) ) {
346 + return;
347 + }
348 +
349 + $plugin_files = array_keys( $this->groups[ $this->current_group ] );
350 + $controls = [];
351 + $button_class = $this->config['params']['navStyle'] === 'groups-dropdown' ? 'action' : 'button-small';
352 +
353 + if ( current_user_can( 'activate_plugins' ) ) {
354 + $inactive = array_filter(
355 + $plugin_files,
356 + function ( $plugin ) {
357 +
358 + return ! is_plugin_active( $plugin );
359 + }
360 + );
361 + if ( ! empty( $inactive ) ) {
362 + $controls[] = $this->build_bulk_action_form( 'activate-selected', $inactive, __( 'Activate All', self::$slug ) );
363 + }
364 + }
365 +
366 + if ( current_user_can( 'deactivate_plugins' ) ) {
367 + $active = array_filter( $plugin_files, 'is_plugin_active' );
368 + if ( ! empty( $active ) ) {
369 + $controls[] = $this->build_bulk_action_form( 'deactivate-selected', $active, __( 'Deactivate All', self::$slug ) );
370 + }
371 + }
372 +
373 + if ( current_user_can( 'update_plugins' ) ) {
374 + $updates = get_site_transient( 'update_plugins' );
375 + $updatable = ! empty( $updates->response ) ? array_intersect( $plugin_files, array_keys( $updates->response ) ) : [];
376 + if ( ! empty( $updatable ) ) {
377 + $url = wp_nonce_url(
378 + self_admin_url( 'update.php?action=update-selected&plugins=' . rawurlencode( implode( ',', $updatable ) ) ),
379 + 'bulk-update-plugins'
380 + );
381 +
382 + $controls[] = Utils::build_tag(
383 + 'a',
384 + [
385 + 'href' => $url,
386 + 'class' => 'button button-secondary ' . $button_class,
387 + ],
388 + esc_html__( 'Update All', self::$slug )
389 + );
390 + }
391 + }
392 +
393 + if ( empty( $controls ) ) {
394 + return;
395 + }
396 +
397 + echo wp_kses(
398 + Utils::build_tag( 'div', [ 'class' => 'group-bulk-actions' ], implode( ' ', $controls ) ),
399 + array_merge(
400 + wp_kses_allowed_html( 'post' ),
401 + [
402 + 'form' => [
403 + 'method' => true,
404 + 'action' => true,
405 + 'style' => true,
406 + ],
407 + 'input' => [
408 + 'type' => true,
409 + 'name' => true,
410 + 'value' => true,
411 + ],
412 + 'button' => [
413 + 'type' => true,
414 + 'class' => true,
415 + ],
416 + ]
417 + )
418 + );
419 + }
420 +
421 + /**
422 + * Build a small inline POST form for a plugins.php bulk action (activate/deactivate-selected).
423 + *
424 + * @param string $action The plugins.php bulk action slug.
425 + * @param array $plugin_files The plugin files to act on.
426 + * @param string $label The button label.
427 + *
428 + * @return string
429 + */
430 + protected function build_bulk_action_form( $action, array $plugin_files, $label ) {
431 +
432 + $inputs = '';
433 + $button_class = $this->config['params']['navStyle'] === 'groups-dropdown' ? 'action' : 'button-small';
434 + foreach ( $plugin_files as $plugin_file ) {
435 + $inputs .= Utils::build_tag(
436 + 'input',
437 + [
438 + 'type' => 'hidden',
439 + 'name' => 'checked[]',
440 + 'value' => $plugin_file,
441 + ]
442 + );
443 + }
444 + $inputs .= wp_nonce_field( 'bulk-plugins', '_wpnonce', true, false );
445 + $inputs .= Utils::build_tag(
446 + 'input',
447 + [
448 + 'type' => 'hidden',
449 + 'name' => 'action',
450 + 'value' => $action,
451 + ]
452 + );
453 + $inputs .= Utils::build_tag( 'button', [
454 + 'type' => 'submit',
455 + 'class' => 'button button-secondary ' . $button_class,
456 + ], esc_html( $label ) );
457 +
458 + return Utils::build_tag(
459 + 'form',
460 + [
461 + 'method' => 'post',
462 + 'action' => self_admin_url( 'plugins.php' ),
463 + 'style' => 'display:inline-block;',
464 + ],
465 + $inputs
466 + );
467 + }
468 +
469 + /**
311 470 * Catch the selected group.
312 471 *
313 472 * @param array $plugins List of plugins.
314 473 *
@@ -313,14 +472,14 @@
313 472 * @param array $plugins List of plugins.
314 473 *
315 474 * @return array
316 475 */
317 - public function catch_selected_group( $plugins = array() ) {
476 + public function catch_selected_group( $plugins = [] ) {
318 477
319 478 global $status;
320 479
321 480 $this->current_status = $status;
322 - $selected_group = filter_input( INPUT_GET, self::$slug, FILTER_SANITIZE_STRING );
481 + $selected_group = Utils::get_sanitized_text( INPUT_GET, self::$slug );
323 482 if ( $selected_group && isset( $this->groups[ $selected_group ] ) ) {
324 483 $this->current_group = $selected_group;
325 484 if ( ! empty( $plugins ) ) {
326 485 $plugins = $this->groups[ $selected_group ];
@@ -346,10 +505,10 @@
346 505 * @return string
347 506 */
348 507 protected function get_nav_url( $id = null ) {
349 508
350 - $url = filter_input( INPUT_SERVER, 'REQUEST_URI', FILTER_SANITIZE_URL );
351 - if ( ! $url || false === strpos( 'plugins.php', $url ) ) {
509 + $url = Utils::get_sanitized_text( INPUT_SERVER, 'REQUEST_URI' );
510 + if ( ! $url || false === strpos( $url, 'plugins.php' ) ) {
352 511 // Just in case.
353 512 $url = self_admin_url( 'plugins.php' );
354 513 }
355 514 if ( null === $id ) {
@@ -362,13 +521,13 @@
362 521 return add_query_arg( 'page', self::$slug, $url );
363 522 }
364 523
365 524 return add_query_arg(
366 - array(
525 + [
367 526 'plugin_status' => $this->current_status,
368 527 self::$slug => $id,
369 528
370 - ),
529 + ],
371 530 $url
372 531 );
373 532 }
374 533
@@ -410,19 +569,19 @@
410 569 $counter = '';
411 570
412 571 if ( ! empty( $total ) ) {
413 572
414 - $counter = Utils::build_tag( 'span', array( 'class' => 'count' ), " ({$total})" );
573 + $counter = Utils::build_tag( 'span', [ 'class' => 'count' ], " ({$total})" );
415 574 }
416 - $li_atts = array(
417 - 'class' => array(
575 + $li_atts = [
576 + 'class' => [
418 577 'group-link',
419 578 $group['id'],
420 - ),
421 - );
422 - $link_atts = array(
579 + ],
580 + ];
581 + $link_atts = [
423 582 'href' => $this->get_nav_url( $id ),
424 - );
583 + ];
425 584 if ( $group['id'] === $this->current_group ) {
426 585 $li_atts['class'][] = 'current';
427 586 $link_atts['class'] = 'current';
428 587 $link_atts['aria-current'] = 'page';
@@ -427,10 +586,22 @@
427 586 $link_atts['class'] = 'current';
428 587 $link_atts['aria-current'] = 'page';
429 588 }
430 589
431 - $link = Utils::build_tag( 'a', $link_atts, $group['name'] . $counter );
590 + $color_dot = '';
591 + if ( ! empty( $group['color'] ) ) {
592 + $color_dot = Utils::build_tag(
593 + 'span',
594 + [
595 + 'class' => 'group-color-dot',
596 + 'style' => 'background-color: ' . $group['color'],
597 + ],
598 + ''
599 + );
600 + }
432 601
602 + $link = Utils::build_tag( 'a', $link_atts, $color_dot . esc_html( $group['name'] ) . $counter );
603 +
433 604 return Utils::build_tag( 'li', $li_atts, $link );
434 605 }
435 606
436 607 /**
@@ -439,30 +610,56 @@
439 610 * @return array
440 611 */
441 612 protected function load_presets() {
442 613
443 - $groups = array(
444 - 'WooCommerce' => array( 'WooCommerce' ),
445 - 'Easy Digital Downloads' => array( 'Easy Digital Downloads' ),
446 - 'Ninja Forms' => array( 'Ninja Forms' ),
447 - 'Gravity Forms' => array( 'Gravity Forms' ),
448 - 'WPForms' => array( 'WPForms' ),
449 - 'E-Commerce' => array(
614 + $groups = [
615 + 'WooCommerce' => [ 'WooCommerce' ],
616 + 'Easy Digital Downloads' => [ 'Easy Digital Downloads' ],
617 + 'Ninja Forms' => [ 'Ninja Forms' ],
618 + 'Gravity Forms' => [ 'Gravity Forms' ],
619 + 'WPForms' => [ 'WPForms' ],
620 + 'E-Commerce' => [
450 621 'WooCommerce',
451 622 'Easy Digital Downloads',
452 - ),
453 - 'Forms' => array(
623 + ],
624 + 'Forms' => [
454 625 'Ninja Forms',
455 626 'Gravity Forms',
456 627 'WPForms',
457 628 'Formidable Forms',
458 - ),
459 - 'SEO' => array(
629 + 'Contact Form 7',
630 + ],
631 + 'SEO' => [
460 632 'All in One SEO',
461 633 'Yoast SEO',
462 - ),
463 - // @todo: Add more categories for presets.
464 - );
634 + ],
635 + 'Security' => [
636 + 'Wordfence',
637 + 'Sucuri',
638 + 'iThemes Security',
639 + ],
640 + 'Caching' => [
641 + 'WP Rocket',
642 + 'W3 Total Cache',
643 + 'WP Super Cache',
644 + 'WP Fastest Cache',
645 + ],
646 + 'Backups' => [
647 + 'UpdraftPlus',
648 + 'BackWPup',
649 + 'Duplicator',
650 + ],
651 + 'Page Builders' => [
652 + 'Elementor',
653 + 'Beaver Builder',
654 + 'Divi Builder',
655 + ],
656 + 'Email/SMTP' => [
657 + 'WP Mail SMTP',
658 + 'FluentSMTP',
659 + 'Post SMTP',
660 + ],
661 + ];
465 662
466 663 /**
467 664 * Filter the presets (legacy)
468 665 *
@@ -473,30 +670,31 @@
473 670 /**
474 671 * Filter preset plugin groups.
475 672 *
476 673 * @hook get_preset_plugin_groups
477 - * @since 2.0.0
478 674 *
479 675 * @param $groups {array} The preset groups.
480 676 *
481 677 * @return array
678 + * @since 2.0.0
679 + *
482 680 */
483 681 $presets = apply_filters( 'get_preset_plugin_groups', $groups );
484 - $preset_groups = array();
682 + $preset_groups = [];
485 683 foreach ( $presets as $name => $keywords ) {
486 684 $id = sanitize_title( $name );
487 - $preset_groups[ $id ] = array(
685 + $preset_groups[ $id ] = [
488 686 'id' => $id,
489 687 'name' => $name,
490 - 'plugins' => array(),
688 + 'plugins' => [],
491 689 'keywords' => $keywords,
492 - );
690 + ];
493 691 }
494 692
495 - return array(
693 + return [
496 694 'preset_groups' => $preset_groups,
497 695 'presets' => array_keys( $groups ),
498 - );
696 + ];
499 697 }
500 698
501 699 /**
502 700 * Make all link tag.
@@ -504,11 +702,11 @@
504 702 * @return string
505 703 */
506 704 protected function make_all_tag() {
507 705
508 - $link_atts = array(
706 + $link_atts = [
509 707 'href' => $this->get_nav_url(),
510 - );
708 + ];
511 709 if ( empty( $this->current_group ) ) {
512 710 $link_atts['class'] = 'current';
513 711 $link_atts['aria-current'] = 'page';
514 712 }
@@ -513,9 +711,9 @@
513 711 $link_atts['aria-current'] = 'page';
514 712 }
515 713 $link = Utils::build_tag( 'a', $link_atts, __( 'All Groups', self::$slug ) );
516 714
517 - return Utils::build_tag( 'li', array( 'class' => array( 'group-link', '__allgroups' ) ), $link );
715 + return Utils::build_tag( 'li', [ 'class' => [ 'group-link', '__allgroups' ] ], $link );
518 716 }
519 717
520 718 /**
521 719 * Add new filter view
@@ -532,9 +730,9 @@
532 730 $url = add_query_arg( self::$slug, $this->current_group, $atts['href'] );
533 731 $tag = str_replace( $atts['href'], $url, $tag );
534 732 wp_parse_str( wp_parse_url( $atts['href'], PHP_URL_QUERY ), $query );
535 733 if ( isset( $query['plugin_status'] ) && $query['plugin_status'] === $this->current_status ) {
536 - $names = explode( ' ', wp_kses( $tag, array() ) );
734 + $names = explode( ' ', wp_kses( $tag, [] ) );
537 735 array_pop( $names );
538 736 $this->current_nav_path .= ' | ' . implode( ' ', $names );
539 737 }
540 738 }
@@ -548,11 +746,11 @@
548 746 }
549 747
550 748 // @todo: Make method.
551 749 if ( ! empty( $this->current_nav_path ) ) {
552 - $append = array(
750 + $append = [
553 751 'name' => $this->current_nav_path,
554 - );
752 + ];
555 753 wp_add_inline_script(
556 754 'wp-util',
557 755 'var appendHead = ' . wp_json_encode(
558 756 $append
@@ -575,9 +773,9 @@
575 773 if ( is_multisite() ) {
576 774 if ( null === $site_id ) {
577 775 $site_id = get_current_blog_id();
578 776 }
579 - $success = update_network_option( $site_id, self::CONFIG_KEY, $this->config );
777 + $success = update_blog_option( $site_id, self::CONFIG_KEY, $this->config );
580 778 } else {
581 779 $success = update_option( self::CONFIG_KEY, $this->config );
582 780 }
583 781
@@ -584,8 +782,27 @@
584 782 return $success;
585 783 }
586 784
587 785 /**
786 + * Save the multisite access list on the main site config.
787 + *
788 + * @param array $site_ids Site IDs allowed to manage their own groups.
789 + *
790 + * @return bool
791 + */
792 + public function save_sites_enabled( array $site_ids ) {
793 +
794 + $config = is_multisite() ? $this->get_stored_config( get_main_site_id() ) : get_option( self::CONFIG_KEY, $this->get_default_config() );
795 + $config['sitesEnabled'] = array_values( array_map( 'intval', $site_ids ) );
796 +
797 + if ( is_multisite() ) {
798 + return update_blog_option( get_main_site_id(), self::CONFIG_KEY, $config );
799 + }
800 +
801 + return update_option( self::CONFIG_KEY, $config );
802 + }
803 +
804 + /**
588 805 * Autoloader by Locating and finding classes via folder structure.
589 806 *
590 807 * @param string $class class name to be checked and autoloaded.
591 808 */
@@ -639,9 +856,9 @@
639 856 $previous_version = get_option( self::VERSION_KEY, 0.0 );
640 857 $new_version = $this->version();
641 858 if ( version_compare( $previous_version, $new_version, '<' ) ) {
642 859 if ( version_compare( $previous_version, '2.0.0', '<' ) ) {
643 - $data = get_option( 'plugin_groups_plugin_groups', array() );
860 + $data = get_option( 'plugin_groups_plugin_groups', [] );
644 861 if ( ! empty( $data ) ) {
645 862 $new_config = $this->convert_legacy_groups( $data );
646 863 update_option( self::CONFIG_KEY, $new_config );
647 864 }
@@ -661,17 +878,17 @@
661 878 * @return array
662 879 */
663 880 protected function convert_legacy_groups( $data ) {
664 881
665 - $groups = array();
882 + $groups = [];
666 883 foreach ( $data['group'] as $group ) {
667 884 $keywords = explode( "\n", $group['config']['keywords'] );
668 - $new_group = array(
885 + $new_group = [
669 886 'id' => $group['_id'],
670 887 'name' => $group['config']['group_name'],
671 - 'plugins' => isset( $group['config']['plugins'] ) ? $group['config']['plugins'] : array(),
888 + 'plugins' => isset( $group['config']['plugins'] ) ? $group['config']['plugins'] : [],
672 889 'keywords' => array_filter( $keywords ),
673 - );
890 + ];
674 891 $groups[ $group['_id'] ] = $new_group;
675 892 }
676 893
677 894 // Set up the new config.
@@ -676,9 +893,9 @@
676 893
677 894 // Set up the new config.
678 895 $config = $this->get_default_config();
679 896 $config['groups'] = $groups;
680 - $config['selectedPresets'] = isset( $data['presets'] ) ? $data['presets'] : array();
897 + $config['selectedPresets'] = isset( $data['presets'] ) ? $data['presets'] : [];
681 898 $config['params']['legacyGrouping'] = true;
682 899 $config['params']['navStyle'] = 'subsubsub';
683 900
684 901 return $config;
@@ -698,9 +915,9 @@
698 915
699 916 /**
700 917 * Init the settings system
701 918 *
702 - * @param Plugin_Groups ${slug} The core object.
919 + * @param groups\classes\Plugin_Groups ${slug} The core object.
703 920 */
704 921 do_action( 'plugin_groups_init' );
705 922 }
706 923
@@ -708,18 +925,33 @@
708 925 * Hook into admin_init.
709 926 */
710 927 public function admin_init() {
711 928
712 - if ( ! empty( $_POST['plugin-group-config'] ) ) {
713 - $data = stripslashes( $_POST['plugin-group-config'] );
714 - $data = json_decode( $data, true );
715 - update_option( Plugin_Groups::CONFIG_KEY, $data );
716 - wp_safe_redirect( admin_url( 'plugins.php?page=plugin-groups' ) );
929 + $manifest_path = PLGGRP_PATH . 'build/manifest.json';
930 + $navbar_manifest_path = PLGGRP_PATH . 'static/manifest.json';
931 + if ( ! file_exists( $manifest_path ) || ! file_exists( $navbar_manifest_path ) ) {
932 + wp_die(
933 + __( 'The Plugin Groups build is missing. Please run the build process.', 'plugin-groups' )
934 + );
717 935 }
718 - $asset = include PLGGRP_PATH . 'js/' . self::$slug . '.asset.php';
719 - wp_register_script( self::$slug, PLGGRP_URL . 'js/' . self::$slug . '.js', $asset['dependencies'], $asset['version'], true );
720 - wp_register_style( self::$slug, PLGGRP_URL . 'css/' . self::$slug . '.css', array(), $asset['version'] );
721 - wp_register_style( self::$slug . '-navbar', PLGGRP_URL . 'css/' . self::$slug . '-navbar.css', array(), $asset['version'] );
936 + $manifest = json_decode( file_get_contents( $manifest_path ), true );
937 + $navbar_manifest = json_decode( file_get_contents( $navbar_manifest_path ), true );
938 + if ( ! isset( $manifest['src/main.tsx'] ) || ! isset( $navbar_manifest['src/extras.js'] ) ) {
939 + wp_die(
940 + __( 'The Plugin Groups build is invalid. Please run the build process again.', 'plugin-groups' )
941 + );
942 + }
943 + $js_path = PLGGRP_URL . 'build/' . $manifest['src/main.tsx']['file'];
944 + wp_register_script( self::$slug, $js_path, [], PLGGRP_VERSION, [ 'in_footer' => true ] );
945 +
946 + if ( ! empty( $manifest['src/main.tsx']['css'] ) ) {
947 + $css_path = PLGGRP_URL . 'build/' . $manifest['src/main.tsx']['css'][0] ?? '';
948 + wp_register_style( self::$slug, $css_path, [], PLGGRP_VERSION );
949 + }
950 + if ( ! empty( $navbar_manifest['src/extras.js']['css'] ) ) {
951 + $css_path = PLGGRP_URL . 'static/' . $navbar_manifest['src/extras.js']['css'][0] ?? '';
952 + wp_register_style( self::$slug . '-navbar', $css_path, [], PLGGRP_VERSION );
953 + }
722 954 }
723 955
724 956 /**
725 957 * Hook into the admin_menu.
@@ -726,9 +958,12 @@
726 958 */
727 959 public function admin_menu() {
728 960
729 961 if ( ! $this->network_active() || $this->site_enabled() || is_network_admin() ) {
730 - add_submenu_page( 'plugins.php', __( 'Plugin Groups', self::$slug ), __( 'Plugin Groups', self::$slug ), 'manage_options', 'plugin-groups', array( $this, 'render_admin' ), 50 );
962 + add_submenu_page( 'plugins.php', __( 'Plugin Groups', self::$slug ), __( 'Plugin Groups', self::$slug ), 'manage_options', 'plugin-groups', [
963 + $this,
964 + 'render_admin',
965 + ], 50 );
731 966 }
732 967 }
733 968
734 969 /**
@@ -747,12 +982,36 @@
747 982 * @return bool
748 983 */
749 984 protected function site_enabled() {
750 985
751 - $site_id = get_current_blog_id();
752 - $main_config = get_network_option( get_main_site_id(), self::CONFIG_KEY, $this->get_default_config() );
986 + return $this->can_manage_site_config( get_current_blog_id() );
987 + }
753 988
754 - return in_array( $site_id, $main_config['sitesEnabled'], true );
989 + /**
990 + * Check whether the current user can manage Plugin Groups config for a site.
991 + *
992 + * @param int|null $site_id Site ID to check. Null uses current site.
993 + *
994 + * @return bool
995 + */
996 + public function can_manage_site_config( $site_id = null ) {
997 +
998 + if ( ! is_multisite() ) {
999 + return current_user_can( 'manage_options' );
1000 + }
1001 +
1002 + if ( null === $site_id ) {
1003 + $site_id = get_current_blog_id();
1004 + }
1005 +
1006 + $site_id = (int) $site_id;
1007 + if ( current_user_can( 'manage_network_options' ) ) {
1008 + return true;
1009 + }
1010 +
1011 + $main_config = $this->get_stored_config( get_main_site_id() );
1012 +
1013 + return current_user_can_for_site( $site_id, 'manage_options' ) && in_array( $site_id, $main_config['sitesEnabled'], true );
755 1014 }
756 1015
757 1016 /**
758 1017 * Add Groups to the admin bar.
@@ -764,27 +1023,27 @@
764 1023 if ( ! $this->config['params']['menuGroups'] || ! current_user_can( 'activate_plugins' ) ) {
765 1024 return;
766 1025 }
767 1026
768 - $groups = array(
769 - 'parent' => array(
1027 + $groups = [
1028 + 'parent' => [
770 1029 'id' => self::$slug,
771 1030 'title' => $this->plugin_name,
772 1031 'href' => $this->get_nav_url( 'dashboard' ),
773 - 'meta' => array(
1032 + 'meta' => [
774 1033 'title' => $this->plugin_name,
775 - ),
776 - ),
777 - );
1034 + ],
1035 + ],
1036 + ];
778 1037
779 1038 foreach ( $this->groups as $key => $plugins ) {
780 1039 $group = $this->config['groups'][ $key ];
781 - $groups[ $key ] = array(
1040 + $groups[ $key ] = [
782 1041 'id' => $key,
783 1042 'parent' => self::$slug,
784 1043 'title' => $group['name'] . ' (' . count( $plugins ) . ')',
785 1044 'href' => $this->get_nav_url( $key ),
786 - );
1045 + ];
787 1046 }
788 1047
789 1048 foreach ( $groups as $option ) {
790 1049 $admin_bar->add_menu( $option );
@@ -795,13 +1054,19 @@
795 1054 * Enqueue assets where needed.
796 1055 */
797 1056 public function enqueue_assets() {
798 1057
799 - $page = filter_input( INPUT_GET, 'page', FILTER_SANITIZE_STRING );
1058 + $page = Utils::get_sanitized_text( INPUT_GET, 'page' );
800 1059 if ( $page && self::$slug === $page ) {
801 - wp_enqueue_script( self::$slug );
802 - wp_enqueue_style( self::$slug );
803 - wp_set_script_translations( self::$slug, self::$slug );
1060 +
1061 + // In development mode, load from Vite dev server.
1062 + if ( $this->dev_mode && file_exists( PLGGRP_PATH . '/dev/dev-server-asset.php' ) ) {
1063 + require_once PLGGRP_PATH . '/dev/dev-server-asset.php';
1064 + } else {
1065 + wp_enqueue_script( self::$slug );
1066 + wp_enqueue_style( self::$slug );
1067 + wp_set_script_translations( self::$slug, self::$slug );
1068 + }
804 1069 $this->prep_config();
805 1070 }
806 1071 }
807 1072
@@ -860,19 +1125,20 @@
860 1125 * @return array
861 1126 */
862 1127 protected function get_default_config() {
863 1128
864 - return array(
865 - 'groups' => array(),
866 - 'selectedPresets' => array(),
867 - 'params' => array(
868 - 'legacyGrouping' => false,
869 - 'navStyle' => 'subsubsub',
870 - 'menuGroups' => false,
871 - ),
1129 + return [
1130 + 'groups' => [],
1131 + 'selectedPresets' => [],
1132 + 'params' => [
1133 + 'legacyGrouping' => false,
1134 + 'navStyle' => 'subsubsub',
1135 + 'menuGroups' => false,
1136 + 'groupBulkActions' => false,
1137 + ],
872 1138
873 - 'sitesEnabled' => array(), // Used for multisite.
874 - );
1139 + 'sitesEnabled' => [], // Used for multisite.
1140 + ];
875 1141 }
876 1142
877 1143 /**
878 1144 * Load the UI config.
@@ -887,9 +1153,9 @@
887 1153 if ( is_multisite() ) {
888 1154 if ( ! $site_id ) {
889 1155 $site_id = get_current_blog_id();
890 1156 }
891 - $config = get_network_option( $site_id, self::CONFIG_KEY, $this->get_default_config() );
1157 + $config = $this->get_stored_config( $site_id );
892 1158 $config['siteID'] = (int) $site_id;
893 1159 } else {
894 1160 $config = get_option( self::CONFIG_KEY, $this->get_default_config() );
895 1161 }
@@ -900,16 +1166,37 @@
900 1166 // Flag if network admin vs main site.
901 1167 if ( is_network_admin() && is_main_site() ) {
902 1168 $config['networkAdmin'] = true;
903 1169 }
1170 + $presets = $this->load_presets();
904 1171
905 - // Load the presets.
906 - $config += $this->load_presets();
1172 + $config['presets'] = $presets['presets'];
1173 + $config['preset_groups'] = $presets['preset_groups'];
907 1174
908 1175 return $config;
909 1176 }
910 1177
911 1178 /**
1179 + * Get persisted config for a multisite blog.
1180 + *
1181 + * Falls back to the legacy network-option storage location used by earlier
1182 + * builds before saving back to the blog's own options table.
1183 + *
1184 + * @param int $site_id Site ID to load.
1185 + *
1186 + * @return array
1187 + */
1188 + protected function get_stored_config( $site_id ) {
1189 +
1190 + $config = get_blog_option( (int) $site_id, self::CONFIG_KEY, false );
1191 + if ( false === $config ) {
1192 + $config = get_network_option( (int) $site_id, self::CONFIG_KEY, $this->get_default_config() );
1193 + }
1194 +
1195 + return wp_parse_args( $config, $this->get_default_config() );
1196 + }
1197 +
1198 + /**
912 1199 * Set the config.
913 1200 *
914 1201 * @param array $config The config to set.
915 1202 * @param int|null $site_id The site ID to load. Null for current site.
@@ -923,12 +1210,12 @@
923 1210 $this->config['groups']['__ungrouped'] = $ungrouped;
924 1211 }
925 1212 }
926 1213 // Populate groups with plugins.
927 - array_map( array( $this, 'populate_plugins' ), $this->config['groups'] );
1214 + array_map( [ $this, 'populate_plugins' ], $this->config['groups'] );
928 1215 // register selected presets.
929 1216 if ( ! empty( $this->config['selectedPresets'] ) ) {
930 - array_map( array( $this, 'register_preset' ), $this->config['selectedPresets'] );
1217 + array_map( [ $this, 'register_preset' ], $this->config['selectedPresets'] );
931 1218 }
932 1219 }
933 1220
934 1221 /**
@@ -937,14 +1224,14 @@
937 1224 * @return array
938 1225 */
939 1226 protected function create_ungrouped() {
940 1227 $plugins = array_keys( get_plugins() );
941 - $new_group = array(
1228 + $new_group = [
942 1229 'id' => '__ungrouped',
943 1230 'name' => __( 'Ungrouped', self::$slug ),
944 - 'plugins' => array(),
945 - );
946 - $grouped = array();
1231 + 'plugins' => [],
1232 + ];
1233 + $grouped = [];
947 1234 foreach ( $this->config['groups'] as $group ) {
948 1235 $grouped = array_merge( $grouped, $group['plugins'] );
949 1236 }
950 1237 $grouped = array_unique( $grouped );
@@ -978,11 +1265,11 @@
978 1265 $plugins = get_plugins();
979 1266 }
980 1267 foreach ( $group['plugins'] as $plugin ) {
981 1268 if ( ! isset( $plugins[ $plugin ] ) ) {
982 - $this->missing[ $plugin ] = array(
1269 + $this->missing[ $plugin ] = [
983 1270 'Version' => '0.0',
984 - );
1271 + ];
985 1272 continue;
986 1273 }
987 1274 $this->groups[ $group['id'] ][ $plugin ] = $plugins[ $plugin ];
988 1275 }
@@ -999,14 +1286,30 @@
999 1286
1000 1287 if ( ! isset( $this->config['groups'][ $id ] ) || empty( $this->config['groups'][ $id ]['keywords'] ) ) {
1001 1288 return;
1002 1289 }
1003 - $keywords = array_map( 'strtolower', $this->config['groups'][ $id ]['keywords'] );
1004 - $plugins = get_plugins();
1290 + $matches = $this->match_plugins_by_keywords( $this->config['groups'][ $id ]['keywords'], get_plugins() );
1291 + foreach ( $matches as $plugin_key => $plugin_data ) {
1292 + if ( ! isset( $this->groups[ $id ][ $plugin_key ] ) ) {
1293 + $this->groups[ $id ][ $plugin_key ] = $plugin_data;
1294 + }
1295 + }
1296 + }
1297 +
1298 + /**
1299 + * Match plugins against a list of keywords, substring-matched (case-insensitive)
1300 + * against each plugin's concatenated header metadata.
1301 + *
1302 + * @param array $keywords The keywords to match.
1303 + * @param array $plugins Plugin file => plugin data map, as returned by get_plugins().
1304 + *
1305 + * @return array Plugin file => plugin data map of matched plugins.
1306 + */
1307 + protected function match_plugins_by_keywords( array $keywords, array $plugins ) {
1308 +
1309 + $keywords = array_map( 'strtolower', $keywords );
1310 + $matches = [];
1005 1311 foreach ( $plugins as $plugin_key => $plugin_data ) {
1006 - if ( isset( $this->groups[ $id ][ $plugin_key ] ) ) {
1007 - continue;
1008 - }
1009 1312 $plugin_string = strtolower( implode( ' ', $plugin_data ) );
1010 1313 $matched = array_filter(
1011 1314 $keywords,
1012 1315 function ( $keyword ) use ( $plugin_string ) {
@@ -1014,14 +1317,74 @@
1014 1317 return false !== strpos( $plugin_string, $keyword );
1015 1318 }
1016 1319 );
1017 1320 if ( ! empty( $matched ) ) {
1018 - $this->groups[ $id ][ $plugin_key ] = $plugin_data;
1321 + $matches[ $plugin_key ] = $plugin_data;
1019 1322 }
1020 1323 }
1324 +
1325 + return $matches;
1021 1326 }
1022 1327
1023 1328 /**
1329 + * Persist keyword-matched plugins into a group's (or all groups') saved plugin list.
1330 + *
1331 + * Unlike populate_keywords(), which only affects the runtime nav/count state, this
1332 + * writes matches into $this->config['groups'][$id]['plugins'] and saves the config.
1333 + *
1334 + * @param string|null $group_id Optional group ID to limit to; null runs for every group with keywords.
1335 + *
1336 + * @return bool True if any group was updated and saved.
1337 + */
1338 + public function auto_assign_by_keywords( $group_id = null ) {
1339 +
1340 + $plugins = get_plugins();
1341 + $group_ids = null !== $group_id ? [ $group_id ] : array_keys( $this->config['groups'] );
1342 +
1343 + $claimed = [];
1344 + foreach ( $this->config['groups'] as $group ) {
1345 + $claimed = array_merge( $claimed, $group['plugins'] );
1346 + }
1347 +
1348 + $updated = false;
1349 + foreach ( $group_ids as $id ) {
1350 + if ( ! isset( $this->config['groups'][ $id ] ) || empty( $this->config['groups'][ $id ]['keywords'] ) ) {
1351 + continue;
1352 + }
1353 + $matches = $this->match_plugins_by_keywords( $this->config['groups'][ $id ]['keywords'], $plugins );
1354 + $new_plugins = array_diff( array_keys( $matches ), $claimed );
1355 + if ( empty( $new_plugins ) ) {
1356 + continue;
1357 + }
1358 + $this->config['groups'][ $id ]['plugins'] = array_values(
1359 + array_unique( array_merge( $this->config['groups'][ $id ]['plugins'], $new_plugins ) )
1360 + );
1361 + $claimed = array_merge( $claimed, $new_plugins );
1362 + $updated = true;
1363 + }
1364 +
1365 + if ( $updated ) {
1366 + $this->save_config();
1367 + }
1368 +
1369 + return $updated;
1370 + }
1371 +
1372 + /**
1373 + * Re-run keyword auto-assignment for all groups after a plugin is installed or updated.
1374 + *
1375 + * @param \WP_Upgrader $upgrader The upgrader instance (unused).
1376 + * @param array $hook_extra Extra data describing the upgrade action.
1377 + */
1378 + public function after_plugin_upgrade( $upgrader, $hook_extra ) {
1379 +
1380 + if ( ! isset( $hook_extra['type'] ) || 'plugin' !== $hook_extra['type'] ) {
1381 + return;
1382 + }
1383 + $this->auto_assign_by_keywords();
1384 + }
1385 +
1386 + /**
1024 1387 * Create a new Group.
1025 1388 *
1026 1389 * @param string $name The group name to create.
1027 1390 * @param array $plugin_slugs Plugin slug or list of slugs to add to the group.
@@ -1027,19 +1390,20 @@
1027 1390 * @param array $plugin_slugs Plugin slug or list of slugs to add to the group.
1028 1391 *
1029 1392 * @return string|bool
1030 1393 */
1031 - public function create_group( $name, array $plugin_slugs = array() ) {
1394 + public function create_group( $name, array $plugin_slugs = [] ) {
1032 1395
1033 1396 $success = false;
1034 1397 $new_id = Utils::generate_id();
1035 - $group = array(
1398 + $group = [
1036 1399 'id' => $new_id,
1037 - 'keywords' => array(),
1400 + 'color' => '',
1401 + 'keywords' => [],
1038 1402 'name' => trim( $name ),
1039 1403 'open' => false,
1040 1404 'plugins' => $plugin_slugs,
1041 - );
1405 + ];
1042 1406 $this->config['groups'][ $new_id ] = $group;
1043 1407 if ( $this->save_config() ) {
1044 1408 $success = $new_id;
1045 1409 }
@@ -1090,8 +1454,18 @@
1090 1454 * Render the admin page.
1091 1455 */
1092 1456 public function render_admin() {
1093 1457
1458 + $bootstrap = [
1459 + 'loadURL' => rest_url( self::$slug . '/load' ),
1460 + 'restNonce' => wp_create_nonce( 'wp_rest' ),
1461 + 'siteID' => get_current_blog_id(),
1462 + ];
1463 +
1464 + // Flag if network admin vs main site.
1465 + if ( is_network_admin() && is_main_site() ) {
1466 + $bootstrap['networkAdmin'] = true;
1467 + }
1094 1468 include PLGGRP_PATH . 'includes/main.php';
1095 1469 }
1096 1470
1097 1471 /**
@@ -1096,9 +1470,9 @@
1096 1470
1097 1471 /**
1098 1472 * Get the instance of the class.
1099 1473 *
1100 - * @return Plugin_Groups
1474 + * @return groups\classes\Plugin_Groups
1101 1475 */
1102 1476 public static function get_instance() {
1103 1477
1104 1478 if ( is_null( self::$instance ) ) {