Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 203 additions & 0 deletions tests/phpunit/tests/admin/includes/plugin/validateActivePlugins.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
<?php

/**
* Tests for validate_active_plugins().
*
* @group admin
* @group plugins
*
* @covers ::validate_active_plugins
* @covers ::validate_plugin
*/
class Tests_Admin_Includes_Plugin_ValidateActivePlugins extends WP_UnitTestCase {

/**
* The directory of the test plugin without a header.
*
* @var string
*/
const NO_HEADER_DIR = 'validate-test-no-header';

/**
* Super admin user ID.
*
* @var int
*/
private static $super_admin_id;

/**
* Sets up the user.
*
* @param WP_UnitTest_Factory $factory Test factory.
*/
public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
self::$super_admin_id = $factory->user->create( array( 'role' => 'administrator' ) );

if ( is_multisite() ) {
grant_super_admin( self::$super_admin_id );
}
}

/**
* Revokes the super admin grant before the fixture users are deleted.
*/
public static function wpTearDownAfterClass() {
if ( is_multisite() ) {
revoke_super_admin( self::$super_admin_id );
}
}

/**
* Removes stale plugin files and resets the active plugins.
*/
public function set_up() {
parent::set_up();

$this->remove_plugin();

wp_cache_delete( 'plugins', 'plugins' );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wp_clean_plugins_cache( false ) is the core helper for clearing this cache. Using it here, in tear_down(), and at line 123 would keep the tests in step if core changes how the plugins cache is stored.

update_site_option( 'active_sitewide_plugins', array() );
}

/**
* Removes the plugin file without a header.
*/
public function tear_down() {
$this->remove_plugin();

wp_cache_delete( 'plugins', 'plugins' );

parent::tear_down();
}

/**
* Tests that an active plugins option that is not an array is reset.
*
* @ticket 65819
*
* @dataProvider data_invalid_options
*
* @param mixed $value The option value.
*/
public function test_should_reset_option_that_is_not_an_array( $value ) {
update_option( 'active_plugins', $value );

$this->assertSame( array(), validate_active_plugins(), 'No invalid plugins should be returned.' );
$this->assertSame( array(), get_option( 'active_plugins' ), 'The option should be reset.' );
}

/**
* Data provider.
*
* @return array[]
*/
public static function data_invalid_options() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this also cover false, null, and an object? Those are more likely values for a corrupt option than a string or an integer. They would also catch a change from the is_array() guard to a looser check.

return array(
'string' => array( 'hello.php' ),
'integer' => array( 1 ),
);
}

/**
* Tests that valid active plugins are kept.
*
* @ticket 65819
*/
public function test_should_keep_valid_plugins() {
update_option( 'active_plugins', array( 'hello.php', 'internationalized-plugin.php' ) );

$this->assertSame( array(), validate_active_plugins(), 'No invalid plugins should be returned.' );
$this->assertSame( array( 'hello.php', 'internationalized-plugin.php' ), get_option( 'active_plugins' ), 'The plugins should stay active.' );
}

/**
* Tests that invalid active plugins are reported and deactivated silently.
*
* @ticket 65819
*/
public function test_should_report_and_deactivate_invalid_plugins() {
$dir = WP_PLUGIN_DIR . '/' . self::NO_HEADER_DIR;
$this->assertTrue( mkdir( $dir ), 'The test directory should be created.' );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WP_PLUGIN_DIR is the checked-in tests/phpunit/data/plugins folder in tests. The folder name is fixed, so two runs at the same time can delete each other's folder. If a run crashes, the folder stays in the repo. This assertion also makes the test error instead of skip when the folder is read-only.

$this->assertNotFalse( file_put_contents( $dir . '/functions.php', "<?php\n// No plugin header.\n" ), 'The file should be written.' );

wp_cache_delete( 'plugins', 'plugins' );

$no_header = self::NO_HEADER_DIR . '/functions.php';
update_option( 'active_plugins', array( 'hello.php', '../outside.php', 'missing/missing.php', $no_header ) );

$deactivate = new MockAction();
add_action( 'deactivate_plugin', array( $deactivate, 'action' ) );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validate_active_plugins() calls deactivate_plugins( $plugin, true ). Silent mode also skips deactivated_plugin and deactivate_{$plugin}. Listening to those hooks too would cover the whole silent path.


$invalid = validate_active_plugins();

$this->assertSame( array( '../outside.php', 'missing/missing.php', $no_header ), array_keys( $invalid ), 'Each invalid plugin should be reported.' );
$this->assertSame(
array(
'../outside.php' => array( 'plugin_invalid', 'Invalid plugin path.' ),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These messages come from translated __() strings. Under another locale, these checks fail even though the code works. Checking the error codes alone would be enough.

'missing/missing.php' => array( 'plugin_not_found', 'Plugin file does not exist.' ),
$no_header => array( 'no_plugin_header', 'The plugin does not have a valid header.' ),
),
array_map(
static function ( $error ) {
return array( $error->get_error_code(), $error->get_error_message() );
},
$invalid
),
'Each invalid plugin should have its error.'
);
$this->assertSame( array( 'hello.php' ), array_values( get_option( 'active_plugins' ) ), 'Only the valid plugin should stay active.' );
$this->assertSame( 0, $deactivate->get_call_count(), 'Invalid plugins should be deactivated silently.' );
}

/**
* Tests that network-active plugins are validated for a user who can manage network plugins.
*
* @ticket 65819
*
* @group ms-required
*/
public function test_should_validate_network_plugins_for_network_admin() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could a case be added where the same missing plugin is in both active_plugins and active_sitewide_plugins? validate_active_plugins() merges the two lists with array_merge(). That case would check that the plugin is removed from both options.

wp_set_current_user( self::$super_admin_id );

update_option( 'active_plugins', array() );
update_site_option(
'active_sitewide_plugins',
array(
'hello.php' => 1000,
'missing/missing.php' => 1000,
)
);

$this->assertSame( array( 'missing/missing.php' ), array_keys( validate_active_plugins() ), 'The invalid network plugin should be reported.' );
$this->assertSame( array( 'hello.php' => 1000 ), get_site_option( 'active_sitewide_plugins' ), 'The invalid network plugin should be deactivated.' );
}

/**
* Tests that network-active plugins are not validated for other users.
*
* @ticket 65819
*
* @group ms-required
*/
public function test_should_not_validate_network_plugins_for_other_users() {
wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );

update_option( 'active_plugins', array() );
update_site_option( 'active_sitewide_plugins', array( 'missing/missing.php' => 1000 ) );

$this->assertSame( array(), validate_active_plugins(), 'No plugins should be validated.' );
$this->assertSame( array( 'missing/missing.php' => 1000 ), get_site_option( 'active_sitewide_plugins' ), 'The network plugins should not change.' );
}

/**
* Removes the plugin file without a header, including one left over by an interrupted run.
*/
private function remove_plugin() {
$dir = WP_PLUGIN_DIR . '/' . self::NO_HEADER_DIR;

if ( is_dir( $dir ) ) {
$this->rmdir( $dir );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A raw rmdir() fails with a warning if the folder ever gets a subfolder. $this->rmdir( $dir ); followed by $this->delete_folders( $dir ); handles nested folders.

rmdir( $dir );
}
}
}
Loading