Skip to content

Commit 31325d1

Browse files
authored
Merge branch 'main' into copilot/allow-php-file-install
2 parents dad98c9 + 075dddc commit 31325d1

11 files changed

+244
-6
lines changed

.github/workflows/copilot-setup-steps.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717

1818
steps:
1919
- name: Checkout code
20-
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
20+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
2121

2222
- name: Check existence of composer.json file
2323
id: check_composer_file

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,10 @@ wp plugin is-active <plugin> [--network]
398398

399399
Returns exit code 0 when active, 1 when not active.
400400

401+
If the plugin does not exist but is still in WordPress's active plugins storage
402+
(such as the active plugins option or the sitewide plugins option for network-activated plugins),
403+
a warning will be emitted.
404+
401405
**OPTIONS**
402406

403407
<plugin>

features/plugin-is-active.feature

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
Feature: Check if a WordPress plugin is active
2+
3+
Background:
4+
Given a WP install
5+
6+
Scenario: Check if an active plugin is active
7+
When I run `wp plugin activate akismet`
8+
Then STDOUT should contain:
9+
"""
10+
Success:
11+
"""
12+
13+
When I run `wp plugin is-active akismet`
14+
Then the return code should be 0
15+
16+
Scenario: Check if an inactive plugin is not active
17+
When I run `wp plugin activate akismet`
18+
Then STDOUT should contain:
19+
"""
20+
Success:
21+
"""
22+
23+
When I run `wp plugin deactivate akismet`
24+
Then STDOUT should contain:
25+
"""
26+
Success:
27+
"""
28+
29+
When I try `wp plugin is-active akismet`
30+
Then the return code should be 1
31+
32+
Scenario: Check if a non-existent plugin is not active
33+
When I try `wp plugin is-active non-existent-plugin`
34+
Then the return code should be 1
35+
36+
Scenario: Warn when plugin is in active_plugins but file does not exist
37+
When I run `wp plugin activate akismet`
38+
Then STDOUT should contain:
39+
"""
40+
Success:
41+
"""
42+
43+
When I run `wp plugin is-active akismet`
44+
Then the return code should be 0
45+
46+
# Remove the plugin directory
47+
When I run `wp plugin path akismet --dir`
48+
Then save STDOUT as {PLUGIN_PATH}
49+
50+
When I run `rm -rf {PLUGIN_PATH}`
51+
Then the return code should be 0
52+
53+
# Now the plugin file is gone but still in active_plugins
54+
When I try `wp plugin is-active akismet`
55+
Then STDERR should contain:
56+
"""
57+
Warning: Plugin 'akismet' is marked as active but the plugin file does not exist.
58+
"""
59+
And the return code should be 1
60+
61+
Scenario: Warn when network-activated plugin is in active_sitewide_plugins but file does not exist
62+
Given a WP multisite install
63+
64+
When I run `wp plugin activate akismet --network`
65+
Then STDOUT should contain:
66+
"""
67+
Success:
68+
"""
69+
70+
When I run `wp plugin is-active akismet --network`
71+
Then the return code should be 0
72+
73+
# Remove the plugin directory
74+
When I run `wp plugin path akismet --dir`
75+
Then save STDOUT as {PLUGIN_PATH}
76+
77+
When I run `rm -rf {PLUGIN_PATH}`
78+
Then the return code should be 0
79+
80+
# Now the plugin file is gone but still in active_sitewide_plugins
81+
When I try `wp plugin is-active akismet --network`
82+
Then STDERR should contain:
83+
"""
84+
Warning: Plugin 'akismet' is marked as active but the plugin file does not exist.
85+
"""
86+
And the return code should be 1

features/plugin-list-wporg-status.feature

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ Feature: Check the status of plugins on WordPress.org
7878
"error": "not_found"
7979
}
8080
"""
81-
And that HTTP requests to https://plugins.trac.wordpress.org/log/never-wporg?limit=1&mode=stop_on_copy&format=rss will respond with:
81+
And that HTTP requests to https://plugins.trac.wordpress.org/log/never-wporg/?limit=1&mode=stop_on_copy&format=rss will respond with:
8282
"""
8383
HTTP/1.1 404
8484
Content-Type: application/rss+xml;charset=utf-8

features/plugin-update.feature

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,3 +267,25 @@ Feature: Update WordPress plugins
267267
"""
268268
Success: Updated 1 of 1 plugins (1 skipped).
269269
"""
270+
271+
@require-wp-5.2
272+
Scenario: Updating all plugins should show the name of each plugin as it is updated
273+
Given a WP install
274+
And I run `wp plugin delete akismet`
275+
276+
When I run `wp plugin install health-check --version=1.5.0`
277+
Then STDOUT should not be empty
278+
279+
When I run `wp plugin install wordpress-importer --version=0.5`
280+
Then STDOUT should not be empty
281+
282+
When I try `wp plugin update --all`
283+
Then STDOUT should contain:
284+
"""
285+
Updating Health Check & Troubleshooting...
286+
"""
287+
288+
And STDOUT should contain:
289+
"""
290+
Success: Updated 2 of 2 plugins.
291+
"""

features/theme-update.feature

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,25 @@ Feature: Update WordPress themes
154154
"""
155155
1.1.1
156156
"""
157+
158+
@require-wp-4.5
159+
Scenario: Updating all themes should show the name of each theme as it is updated
160+
Given a WP install
161+
And I run `wp theme delete --all --force`
162+
163+
When I run `wp theme install moina --version=1.0.2`
164+
Then STDOUT should not be empty
165+
166+
When I run `wp theme install twentytwelve --version=1.0`
167+
Then STDOUT should not be empty
168+
169+
When I try `wp theme update --all`
170+
Then STDOUT should contain:
171+
"""
172+
Updating Moina...
173+
"""
174+
175+
And STDOUT should contain:
176+
"""
177+
Success: Updated 2 of 2 themes.
178+
"""

phpcs.xml.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
<exclude-pattern>*/src/WP_CLI/CommandWithUpgrade\.php$</exclude-pattern>
6262
<exclude-pattern>*/src/WP_CLI/(CommandWith|DestructivePlugin|DestructiveTheme)Upgrader\.php$</exclude-pattern>
6363
<exclude-pattern>*/src/WP_CLI/Parse(Plugin|Theme)NameInput\.php$</exclude-pattern>
64+
<exclude-pattern>*/src/WP_CLI/ExtensionUpgraderSkin\.php$</exclude-pattern>
6465
</rule>
6566

6667
<!-- Exclude classes from UselessOverridingMethod sniff as the method is used for generating docs. -->

src/Plugin_Command.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1221,6 +1221,10 @@ private function get_plugin_dependencies( $slug ) {
12211221
return [];
12221222
}
12231223

1224+
/**
1225+
* @var object{requires_plugins?: array} $api
1226+
*/
1227+
12241228
// Check if requires_plugins field exists and is not empty
12251229
if ( ! empty( $api->requires_plugins ) && is_array( $api->requires_plugins ) ) {
12261230
return $api->requires_plugins;
@@ -1494,6 +1498,10 @@ public function is_installed( $args, $assoc_args ) {
14941498
*
14951499
* Returns exit code 0 when active, 1 when not active.
14961500
*
1501+
* If the plugin does not exist but is still in WordPress's active plugins storage
1502+
* (such as the active plugins option or the sitewide plugins option for network-activated plugins),
1503+
* a warning will be emitted.
1504+
*
14971505
* ## OPTIONS
14981506
*
14991507
* <plugin>
@@ -1517,6 +1525,55 @@ public function is_active( $args, $assoc_args ) {
15171525
$plugin = $this->fetcher->get( $args[0] );
15181526

15191527
if ( ! $plugin ) {
1528+
// Plugin not found via fetcher, but it might still be in active_plugins option
1529+
// Check if it's in the active_plugins list
1530+
$input_name = $args[0];
1531+
// For network plugins: active_sitewide_plugins is an array where keys are plugin files and values are timestamps
1532+
// For regular plugins: active_plugins is an array of plugin file paths
1533+
$active_plugins = $network_wide ? get_site_option( 'active_sitewide_plugins', [] ) : get_option( 'active_plugins', [] );
1534+
1535+
// Ensure we have an array to work with
1536+
if ( ! is_array( $active_plugins ) ) {
1537+
$active_plugins = [];
1538+
}
1539+
1540+
// For network-wide plugins, extract the plugin files from the keys
1541+
if ( $network_wide ) {
1542+
$active_plugin_files = array_keys( $active_plugins );
1543+
} else {
1544+
$active_plugin_files = $active_plugins;
1545+
}
1546+
1547+
// Try to find a matching plugin file in active_plugins using the same logic as the fetcher
1548+
// This matches: exact file name, "name.php", or directory name
1549+
$found_in_active = '';
1550+
foreach ( $active_plugin_files as $plugin_file ) {
1551+
// Ensure plugin_file is a string
1552+
if ( ! is_string( $plugin_file ) ) {
1553+
continue;
1554+
}
1555+
1556+
// Check if the input matches the plugin file in various ways
1557+
// This mirrors the logic in WP_CLI\Fetchers\Plugin::get()
1558+
if (
1559+
"$input_name.php" === $plugin_file ||
1560+
$plugin_file === $input_name ||
1561+
( dirname( $plugin_file ) === $input_name && '.' !== $input_name )
1562+
) {
1563+
$found_in_active = $plugin_file;
1564+
break;
1565+
}
1566+
}
1567+
1568+
if ( $found_in_active ) {
1569+
// Plugin is in active_plugins but file doesn't exist
1570+
// Use validate_plugin to confirm the file is missing
1571+
$validation = validate_plugin( $found_in_active );
1572+
if ( is_wp_error( $validation ) ) {
1573+
WP_CLI::warning( "Plugin '{$input_name}' is marked as active but the plugin file does not exist." );
1574+
}
1575+
}
1576+
15201577
WP_CLI::halt( 1 );
15211578
}
15221579

src/WP_CLI/CommandWithUpgrade.php

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ abstract protected function get_item_list();
8181
*/
8282
abstract protected function filter_item_list( $items, $args );
8383

84+
/**
85+
* @return array<string, array{slug: string, name: string, update: string, recently_active?: bool, status: string, version: string}>
86+
*/
8487
abstract protected function get_all_items();
8588

8689
/**
@@ -266,6 +269,11 @@ public function install( $args, $assoc_args ) {
266269
&& ! preg_match( '#github\.com/[^/]+/[^/]+/(?:releases/download|raw)/#', $slug ) ) {
267270

268271
$filter = function ( $source ) use ( $slug ) {
272+
/**
273+
* @var \WP_Filesystem_Base $wp_filesystem
274+
*/
275+
global $wp_filesystem;
276+
269277
/**
270278
* @var string $path
271279
*/
@@ -284,7 +292,7 @@ public function install( $args, $assoc_args ) {
284292
}
285293
$new_path = substr_replace( $source, $slug_dir, (int) strrpos( $source, $source_dir ), strlen( $source_dir ) );
286294

287-
if ( $GLOBALS['wp_filesystem']->move( $source, $new_path ) ) {
295+
if ( $wp_filesystem->move( $source, $new_path ) ) {
288296
WP_CLI::log( sprintf( "Renamed Github-based project from '%s' to '%s'.", $source_dir, $slug_dir ) );
289297
return $new_path;
290298
}
@@ -535,7 +543,14 @@ protected function get_upgrader( $assoc_args ) {
535543
$force = Utils\get_flag_value( $assoc_args, 'force', false );
536544
$insecure = Utils\get_flag_value( $assoc_args, 'insecure', false );
537545
$upgrader_class = $this->get_upgrader_class( $force );
538-
return Utils\get_upgrader( $upgrader_class, $insecure );
546+
547+
if ( ! class_exists( '\WP_Upgrader_Skin' ) ) {
548+
if ( file_exists( ABSPATH . 'wp-admin/includes/class-wp-upgrader-skin.php' ) ) {
549+
include ABSPATH . 'wp-admin/includes/class-wp-upgrader-skin.php';
550+
}
551+
}
552+
553+
return Utils\get_upgrader( $upgrader_class, $insecure, new ExtensionUpgraderSkin() );
539554
}
540555

541556
protected function update_many( $args, $assoc_args ) {
@@ -1013,6 +1028,9 @@ protected function _search( $args, $assoc_args ) {
10131028
// In older WP versions these used to be objects.
10141029
foreach ( $items as $index => $item_object ) {
10151030
if ( is_array( $item_object ) ) {
1031+
/**
1032+
* @var array{slug: string} $item_object
1033+
*/
10161034
$items[ $index ]['url'] = "https://wordpress.org/{$plural}/{$item_object['slug']}/";
10171035
} elseif ( $item_object instanceof \stdClass ) {
10181036
$item_object->url = "https://wordpress.org/{$plural}/{$item_object->slug}/";
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace WP_CLI;
4+
5+
use WP_CLI;
6+
7+
/**
8+
* An Upgrader Skin for extensions (plugins/themes) that displays which item is being updated
9+
*
10+
* @package wp-cli
11+
*
12+
* @property-read array{Name?: string}|null $plugin_info
13+
* @property-read \WP_Theme|null $theme_info
14+
*/
15+
class ExtensionUpgraderSkin extends UpgraderSkin {
16+
17+
/**
18+
* Called before an update is performed.
19+
*/
20+
public function before() {
21+
// These properties are defined in `Bulk_Plugin_Upgrader_Skin`/`Bulk_Theme_Upgrader_Skin`
22+
if ( isset( $this->plugin_info ) && is_array( $this->plugin_info ) && isset( $this->plugin_info['Name'] ) ) {
23+
WP_CLI::log( sprintf( 'Updating %s...', html_entity_decode( $this->plugin_info['Name'], ENT_QUOTES, get_bloginfo( 'charset' ) ) ) );
24+
} elseif ( isset( $this->theme_info ) && is_object( $this->theme_info ) && method_exists( $this->theme_info, 'get' ) ) {
25+
WP_CLI::log( sprintf( 'Updating %s...', html_entity_decode( $this->theme_info->get( 'Name' ), ENT_QUOTES, get_bloginfo( 'charset' ) ) ) );
26+
}
27+
}
28+
}

0 commit comments

Comments
 (0)