diff --git a/features/check-network-required-plugins.feature b/features/check-network-required-plugins.feature new file mode 100644 index 0000000..f2dc83f --- /dev/null +++ b/features/check-network-required-plugins.feature @@ -0,0 +1,55 @@ +Feature: Check required network plugins + + Scenario: Verify check description + Given an empty directory + And a config.yml file: + """ + network-required-plugins: + check: Network_Required_Plugins + options: + plugins: + - akismet + """ + + When I try `wp doctor list --fields=name,description --config=config.yml` + Then STDOUT should be a table containing rows: + | name | description | + | network-required-plugins | Errors when required plugins are not network-activated. | + + Scenario: Required plugin is not network-activated + Given a WP multisite installation + And a config.yml file: + """ + network-required-plugins: + check: Network_Required_Plugins + options: + plugins: + - akismet + """ + + When I try `wp doctor check network-required-plugins --config=config.yml` + Then STDOUT should be a table containing rows: + | name | status | message | + | network-required-plugins | error | Required network plugin check failed. Not network-activated: akismet (inactive). | + And STDERR should contain: + """ + Error: 1 check reports 'error'. + """ + And the return code should be 1 + + Scenario: Required plugin is network-activated + Given a WP multisite installation + And a config.yml file: + """ + network-required-plugins: + check: Network_Required_Plugins + options: + plugins: + - akismet + """ + And I run `wp plugin activate akismet --network` + + When I run `wp doctor check network-required-plugins --config=config.yml` + Then STDOUT should be a table containing rows: + | name | status | message | + | network-required-plugins | success | All required plugins are network-activated. | diff --git a/features/check-network-site-count.feature b/features/check-network-site-count.feature new file mode 100644 index 0000000..6e466c2 --- /dev/null +++ b/features/check-network-site-count.feature @@ -0,0 +1,46 @@ +Feature: Check multisite network site count + + Scenario: Verify check description + Given an empty directory + And a config.yml file: + """ + network-site-count: + check: Network_Site_Count + """ + + When I try `wp doctor list --fields=name,description --config=config.yml` + Then STDOUT should be a table containing rows: + | name | description | + | network-site-count | Warns when multisite network site count is outside the range 1-500. | + + Scenario: Site count is within expected range + Given a WP multisite installation + And a config.yml file: + """ + network-site-count: + check: Network_Site_Count + options: + minimum: 1 + maximum: 10 + """ + + When I run `wp doctor check network-site-count --config=config.yml` + Then STDOUT should be a table containing rows: + | name | status | message | + | network-site-count | success | Network has 1 sites; expected between 1 and 10. | + + Scenario: Site count is outside expected range + Given a WP multisite installation + And a config.yml file: + """ + network-site-count: + check: Network_Site_Count + options: + minimum: 2 + maximum: 10 + """ + + When I run `wp doctor check network-site-count --config=config.yml` + Then STDOUT should be a table containing rows: + | name | status | message | + | network-site-count | warning | Network has 1 sites; expected between 2 and 10. | diff --git a/features/check-network-site-option-value.feature b/features/check-network-site-option-value.feature new file mode 100644 index 0000000..d47d907 --- /dev/null +++ b/features/check-network-site-option-value.feature @@ -0,0 +1,100 @@ +Feature: Check the value of a network option + + Scenario: Verify check description + Given an empty directory + And a config.yml file: + """ + network-registration: + check: Network_Site_Option_Value + options: + option: registration + value: none + """ + + When I try `wp doctor list --fields=name,description --config=config.yml` + Then STDOUT should be a table containing rows: + | name | description | + | network-registration | Confirms the expected value of the network option 'registration'. | + + Scenario: Check the value of a network option + Given a WP multisite installation + And a config.yml file: + """ + network-registration: + check: Network_Site_Option_Value + options: + option: registration + value: all + """ + And I run `wp eval 'update_site_option( "registration", "none" );'` + + When I try `wp doctor check network-registration --config=config.yml` + Then STDOUT should be a table containing rows: + | name | status | message | + | network-registration | error | Network option 'registration' is 'none' but expected to be 'all'. | + And STDERR should contain: + """ + Error: 1 check reports 'error'. + """ + And the return code should be 1 + + When I run `wp eval 'update_site_option( "registration", "all" );'` + Then STDOUT should be empty + + When I run `wp doctor check network-registration --config=config.yml` + Then STDOUT should be a table containing rows: + | name | status | message | + | network-registration | success | Network option 'registration' is 'all' as expected. | + + Scenario: Check value_is_not for a network option + Given a WP multisite installation + And a config.yml file: + """ + network-registration: + check: Network_Site_Option_Value + options: + option: registration + value_is_not: none + """ + And I run `wp eval 'update_site_option( "registration", "none" );'` + + When I try `wp doctor check network-registration --config=config.yml` + Then STDOUT should be a table containing rows: + | name | status | message | + | network-registration | error | Network option 'registration' is 'none' and expected not to be. | + And STDERR should contain: + """ + Error: 1 check reports 'error'. + """ + And the return code should be 1 + + When I run `wp eval 'update_site_option( "registration", "all" );'` + Then STDOUT should be empty + + When I run `wp doctor check network-registration --config=config.yml` + Then STDOUT should be a table containing rows: + | name | status | message | + | network-registration | success | Network option 'registration' is not 'none' as expected. | + + Scenario: Gracefully render array values in error messages + Given a WP multisite installation + And a config.yml file: + """ + network-registration: + check: Network_Site_Option_Value + options: + option: registration + value: none + """ + And I run `wp eval 'update_site_option( "registration", array( "users" => "all" ) );'` + + When I try `wp doctor check network-registration --config=config.yml` + Then STDOUT should contain: + """ + Network option 'registration' is '{"users":"all"}' but expected to be 'none'. + """ + And STDERR should contain: + """ + Error: 1 check reports 'error'. + """ + And the return code should be 1 diff --git a/src/Check/Network_Required_Plugins.php b/src/Check/Network_Required_Plugins.php new file mode 100644 index 0000000..970bb90 --- /dev/null +++ b/src/Check/Network_Required_Plugins.php @@ -0,0 +1,108 @@ +plugins ) ) { + $this->plugins = explode( ',', $this->plugins ); + } + if ( ! is_array( $this->plugins ) ) { + WP_CLI::error( 'Invalid plugins option. Provide an array or comma-delimited string.' ); + } + $this->plugins = array_values( array_filter( array_map( 'trim', $this->plugins ) ) ); + if ( empty( $this->plugins ) ) { + WP_CLI::error( 'At least one plugin slug is required.' ); + } + } + + public function run() { + if ( ! is_multisite() ) { + $this->set_status( 'success' ); + $this->set_message( 'WordPress is not a multisite installation; required network plugin check skipped.' ); + return; + } + + if ( ! function_exists( 'get_plugins' ) || ! function_exists( 'is_plugin_active' ) || ! function_exists( 'is_plugin_active_for_network' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + $installed_plugins = get_plugins(); + + $missing = array(); + $not_network_active = array(); + foreach ( $this->plugins as $plugin_slug ) { + $plugin_file = $this->get_plugin_file( $plugin_slug, $installed_plugins ); + if ( null === $plugin_file ) { + $missing[] = $plugin_slug; + continue; + } + + if ( is_plugin_active_for_network( $plugin_file ) ) { + continue; + } + + $status = is_plugin_active( $plugin_file ) ? 'active' : 'inactive'; + $not_network_active[] = "{$plugin_slug} ({$status})"; + } + + if ( empty( $missing ) && empty( $not_network_active ) ) { + $this->set_status( 'success' ); + $this->set_message( 'All required plugins are network-activated.' ); + return; + } + + $parts = array(); + if ( ! empty( $missing ) ) { + $parts[] = 'Missing plugins: ' . implode( ', ', $missing ) . '.'; + } + if ( ! empty( $not_network_active ) ) { + $parts[] = 'Not network-activated: ' . implode( ', ', $not_network_active ) . '.'; + } + $this->set_status( 'error' ); + $this->set_message( 'Required network plugin check failed. ' . implode( ' ', $parts ) ); + } + + /** + * Resolve a plugin slug or file to an installed plugin file path. + * + * @param string $plugin_slug Requested plugin slug/file. + * @param array $installed_plugins Installed plugins keyed by plugin file. + * @return string|null + */ + private function get_plugin_file( $plugin_slug, $installed_plugins ) { + if ( isset( $installed_plugins[ $plugin_slug ] ) ) { + return $plugin_slug; + } + + foreach ( array_keys( $installed_plugins ) as $plugin_file ) { + $directory_slug = dirname( $plugin_file ); + if ( '.' !== $directory_slug && $directory_slug === $plugin_slug ) { + return $plugin_file; + } + + $file_slug = basename( $plugin_file, '.php' ); + if ( $file_slug === $plugin_slug ) { + return $plugin_file; + } + } + + return null; + } +} diff --git a/src/Check/Network_Site_Count.php b/src/Check/Network_Site_Count.php new file mode 100644 index 0000000..f2359ef --- /dev/null +++ b/src/Check/Network_Site_Count.php @@ -0,0 +1,55 @@ +minimum = (int) $this->minimum; + $this->maximum = (int) $this->maximum; + if ( $this->minimum < 0 || $this->maximum < 0 || $this->maximum < $this->minimum ) { + WP_CLI::error( 'Invalid thresholds. Ensure 0 <= minimum <= maximum.' ); + } + } + + public function run() { + if ( ! is_multisite() ) { + $this->set_status( 'success' ); + $this->set_message( 'WordPress is not a multisite installation; network site count check skipped.' ); + return; + } + + $count = (int) get_sites( array( 'count' => true ) ); + + if ( $count < $this->minimum || $count > $this->maximum ) { + $this->set_status( 'warning' ); + $this->set_message( "Network has {$count} sites; expected between {$this->minimum} and {$this->maximum}." ); + return; + } + + $this->set_status( 'success' ); + $this->set_message( "Network has {$count} sites; expected between {$this->minimum} and {$this->maximum}." ); + } +} diff --git a/src/Check/Network_Site_Option_Value.php b/src/Check/Network_Site_Option_Value.php new file mode 100644 index 0000000..3b750b7 --- /dev/null +++ b/src/Check/Network_Site_Option_Value.php @@ -0,0 +1,100 @@ +set_status( 'success' ); + $this->set_message( 'WordPress is not a multisite installation; network option check skipped.' ); + return; + } + + if ( isset( $this->value ) && isset( $this->value_is_not ) ) { + $this->set_status( 'error' ); + $this->set_message( 'You must use either "value" or "value_is_not".' ); + return; + } + + if ( ! isset( $this->value ) && ! isset( $this->value_is_not ) ) { + $this->set_status( 'error' ); + $this->set_message( 'You must specify "value" or "value_is_not".' ); + return; + } + + $actual_value = get_site_option( $this->option ); + + if ( isset( $this->value ) ) { + if ( $actual_value == $this->value ) { // phpcs:ignore Universal.Operators.StrictComparisons -- Keep existing behavior. + $this->set_status( 'success' ); + $this->set_message( "Network option '{$this->option}' is '" . $this->format_value_for_message( $this->value ) . "' as expected." ); + } else { + $this->set_status( 'error' ); + $this->set_message( "Network option '{$this->option}' is '" . $this->format_value_for_message( $actual_value ) . "' but expected to be '" . $this->format_value_for_message( $this->value ) . "'." ); + } + return; + } + + if ( $actual_value == $this->value_is_not ) { // phpcs:ignore Universal.Operators.StrictComparisons -- Keep existing behavior. + $this->set_status( 'error' ); + $this->set_message( "Network option '{$this->option}' is '" . $this->format_value_for_message( $actual_value ) . "' and expected not to be." ); + } else { + $this->set_status( 'success' ); + $this->set_message( "Network option '{$this->option}' is not '" . $this->format_value_for_message( $this->value_is_not ) . "' as expected." ); + } + } + + /** + * Format arbitrary option values for stable string output. + * + * @param mixed $value Value to render. + * @return string + */ + private function format_value_for_message( $value ) { + if ( is_array( $value ) || is_object( $value ) ) { + $encoded = wp_json_encode( $value ); + if ( false !== $encoded ) { + return $encoded; + } + return 'unrepresentable value'; + } + + if ( null === $value ) { + return 'null'; + } + if ( true === $value ) { + return 'true'; + } + if ( false === $value ) { + return 'false'; + } + return (string) $value; + } +} diff --git a/src/Command.php b/src/Command.php index 9c76b7f..aeea080 100644 --- a/src/Command.php +++ b/src/Command.php @@ -337,7 +337,9 @@ public function list_( $args, $assoc_args ) { if ( '_' === $prop_name[0] ) { continue; } - $prop->setAccessible( true ); + if ( PHP_VERSION_ID < 80100 ) { + $prop->setAccessible( true ); + } $value = $prop->getValue( $class ); if ( is_array( $value ) ) { $value = json_encode( $value );