Skip to content

Commit 12c5b34

Browse files
committed
Add test_up_to field
1 parent 5619a55 commit 12c5b34

File tree

3 files changed

+270
-1
lines changed

3 files changed

+270
-1
lines changed

features/plugin.feature

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -649,7 +649,7 @@ Feature: Manage WordPress plugins
649649

650650
When I run `wp plugin list --name=hello-dolly --field=version`
651651
And save STDOUT as {PLUGIN_VERSION}
652-
652+
653653
When I run `wp plugin list --name=hello-dolly --field=update_version`
654654
And save STDOUT as {UPDATE_VERSION}
655655

@@ -720,3 +720,63 @@ Feature: Manage WordPress plugins
720720
Then STDOUT should be a table containing rows:
721721
| name | auto_update |
722722
| hello | on |
723+
724+
Scenario: Listing plugins should include tested_up_to from the 'tested up to' header
725+
Given a WP install
726+
And a wp-content/plugins/foo/foo.php file:
727+
"""
728+
<?php
729+
/**
730+
* Plugin Name: Foo
731+
* Description: A plugin for foo
732+
* Author: Matt
733+
*/
734+
"""
735+
And a wp-content/plugins/foo/readme.txt file:
736+
"""
737+
=== Foo ===
738+
Contributors: matt
739+
Donate link: https://example.com/
740+
Tags: tag1, tag2
741+
Requires at least: 4.7
742+
Tested up to: 3.4
743+
Stable tag: 4.3
744+
Requires PHP: 7.0
745+
License: GPLv2 or later
746+
License URI: https://www.gnu.org/licenses/gpl-2.0.html
747+
"""
748+
And I run `wp plugin activate foo`
749+
When I run `wp plugin list --fields=name,tested_up_to`
750+
Then STDOUT should be a table containing rows:
751+
| name | tested_up_to |
752+
| foo | 3.4 |
753+
754+
Scenario: Listing plugins should include tested_up_to from the 'tested' header
755+
Given a WP install
756+
And a wp-content/plugins/foo/foo.php file:
757+
"""
758+
<?php
759+
/**
760+
* Plugin Name: Foo
761+
* Description: A plugin for foo
762+
* Author: Matt
763+
*/
764+
"""
765+
And a wp-content/plugins/foo/readme.txt file:
766+
"""
767+
=== Foo ===
768+
Tested: 5.5
769+
Contributors: matt
770+
Donate link: https://example.com/
771+
Tags: tag1, tag2
772+
Requires at least: 4.7
773+
Stable tag: 4.3
774+
Requires PHP: 7.0
775+
License: GPLv2 or later
776+
License URI: https://www.gnu.org/licenses/gpl-2.0.html
777+
"""
778+
And I run `wp plugin activate foo`
779+
When I run `wp plugin list --fields=name,tested_up_to`
780+
Then STDOUT should be a table containing rows:
781+
| name | tested_up_to |
782+
| foo | 5.5 |

src/Plugin_Command.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22

33
use WP_CLI\ParsePluginNameInput;
4+
use WP_CLI\ParsePluginReadme;
45
use WP_CLI\Utils;
56
use WP_CLI\WpOrgApi;
67

@@ -43,6 +44,7 @@
4344
class Plugin_Command extends \WP_CLI\CommandWithUpgrade {
4445

4546
use ParsePluginNameInput;
47+
use ParsePluginReadme;
4648

4749
protected $item_type = 'plugin';
4850
protected $upgrade_refresh = 'wp_update_plugins';
@@ -59,6 +61,7 @@ class Plugin_Command extends \WP_CLI\CommandWithUpgrade {
5961
'version',
6062
'update_version',
6163
'auto_update',
64+
'tested',
6265
);
6366

6467
/**
@@ -265,6 +268,7 @@ protected function get_all_items() {
265268
'description' => $mu_description,
266269
'file' => $file,
267270
'auto_update' => false,
271+
'tested_up_to' => '',
268272
'wporg_status' => $wporg_info['status'],
269273
'wporg_last_updated' => $wporg_info['last_updated'],
270274
);
@@ -286,6 +290,7 @@ protected function get_all_items() {
286290
'file' => $name,
287291
'auto_update' => false,
288292
'author' => $item_data['Author'],
293+
'tested_up_to' => '',
289294
'wporg_status' => '',
290295
'wporg_last_updated' => '',
291296
];
@@ -739,6 +744,10 @@ protected function get_item_list() {
739744
'wporg_last_updated' => $wporg_info['last_updated'],
740745
];
741746

747+
// Include information from the plugin readme.txt headers.
748+
$plugin_headers = $this->get_plugin_headers( $name );
749+
$items[ $file ]['tested_up_to'] = isset( $plugin_headers['tested_up_to'] ) ? $plugin_headers['tested_up_to'] : '';
750+
742751
if ( null === $update_info ) {
743752
// Get info for all plugins that don't have an update.
744753
$plugin_update_info = isset( $all_update_info->no_update[ $file ] ) ? $all_update_info->no_update[ $file ] : null;
@@ -1251,6 +1260,7 @@ public function delete( $args, $assoc_args = array() ) {
12511260
* * version
12521261
* * update_version
12531262
* * auto_update
1263+
* * tested_up_to
12541264
*
12551265
* These fields are optionally available:
12561266
*

src/WP_CLI/ParsePluginReadme.php

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
<?php
2+
3+
namespace WP_CLI;
4+
5+
use WP_CLI;
6+
7+
trait ParsePluginReadme {
8+
/**
9+
* @var array
10+
*/
11+
private $plugin_headers = [];
12+
13+
/**
14+
* These are the valid header mappings for the header.
15+
*
16+
* @var array
17+
*/
18+
public $valid_plugin_headers = array(
19+
'tested' => 'tested_up_to',
20+
'tested up to' => 'tested_up_to',
21+
);
22+
23+
/**
24+
* Parse readme from the plugin name.
25+
*
26+
* @param string $name The plugin name e.g. "classic-editor".
27+
*/
28+
private function parse_readme( $name = '' ) {
29+
$plugin_readme = WP_PLUGIN_DIR . '/' . $name . '/readme.txt';
30+
31+
if ( ! file_exists( $plugin_readme ) ) {
32+
// Reset the plugin headers if the readme.txt file does not exists
33+
// It ensures that it does not carry out stale data from a previous parsed plugin.
34+
$this->plugin_headers = [];
35+
36+
return;
37+
}
38+
39+
$context = stream_context_create(
40+
array(
41+
'http' => array(
42+
'user_agent' => 'WordPress.org Plugin Readme Parser',
43+
),
44+
)
45+
);
46+
$contents = file_get_contents( $plugin_readme, false, $context );
47+
48+
// At the moment, the parser only concern about parsing the plugin headers, which
49+
// appear after the plugin name header, and before the description header.
50+
if ( preg_match( '/=== .*? ===\s*(.*?)(?:== Description ==|$)/s', $contents, $matches ) ) {
51+
$contents = trim( $matches[1] );
52+
}
53+
54+
if ( preg_match( '!!u', $contents ) ) {
55+
$contents = preg_split( '!\R!u', $contents );
56+
} else {
57+
$contents = preg_split( '!\R!', $contents ); // regex failed due to invalid UTF8 in $contents, see #2298
58+
}
59+
$contents = array_map( array( $this, 'strip_newlines' ), $contents );
60+
61+
// Strip UTF8 BOM if present.
62+
if ( 0 === strpos( $contents[0], "\xEF\xBB\xBF" ) ) {
63+
$contents[0] = substr( $contents[0], 3 );
64+
}
65+
66+
// Convert UTF-16 files.
67+
if ( 0 === strpos( $contents[0], "\xFF\xFE" ) ) {
68+
foreach ( $contents as $i => $line ) {
69+
$contents[ $i ] = mb_convert_encoding( $line, 'UTF-8', 'UTF-16' );
70+
}
71+
}
72+
73+
$line = $this->get_first_nonwhitespace( $contents );
74+
$last_line_was_blank = false;
75+
76+
do {
77+
$value = null;
78+
$header = $this->parse_possible_header( $line );
79+
$line = array_shift( $contents );
80+
81+
// If it doesn't look like a header value, maybe break to the next section.
82+
if ( ! $header ) {
83+
if ( empty( $line ) ) {
84+
// Some plugins have line-breaks within the headers...
85+
$last_line_was_blank = true;
86+
continue;
87+
} else {
88+
// We've hit a line that is not blank, but also doesn't look like a header, assume the Short Description and end Header parsing.
89+
break;
90+
}
91+
}
92+
93+
list( $key, $value ) = $header;
94+
95+
if ( isset( $this->valid_plugin_headers[ $key ] ) ) {
96+
$header_key = $this->valid_plugin_headers[ $key ];
97+
98+
if ( 'tested_up_to' === $header_key && $value ) {
99+
$this->plugin_headers['tested_up_to'] = $this->sanitize_tested_version( $value );
100+
}
101+
102+
$this->plugin_headers[ $this->valid_plugin_headers[ $key ] ] = $value;
103+
} elseif ( $last_line_was_blank ) {
104+
// If we skipped over a blank line, and then ended up with an unexpected header, assume we parsed too far and ended up in the Short Description.
105+
// This final line will be added back into the stack after the loop for further parsing.
106+
break;
107+
}
108+
$last_line_was_blank = false;
109+
} while ( null !== $line );
110+
}
111+
112+
/**
113+
* Gets the plugin header information from the plugin's readme.txt file.
114+
*
115+
* @param string $name The plugin name e.g. "classic-editor".
116+
* @return array
117+
*/
118+
protected function get_plugin_headers( $name ) {
119+
$this->parse_readme( $name );
120+
121+
return $this->plugin_headers;
122+
}
123+
124+
/**
125+
* Parse a line to see if it's a header.
126+
*
127+
* @param string $line The line from the readme to parse.
128+
* @param bool $only_valid Whether to only return a valid known header.
129+
* @return false|array
130+
*/
131+
private function parse_possible_header( $line, $only_valid = false ) {
132+
if ( ! str_contains( $line, ':' ) || str_starts_with( $line, '#' ) || str_starts_with( $line, '=' ) ) {
133+
return false;
134+
}
135+
136+
list( $key, $value ) = explode( ':', $line, 2 );
137+
$key = strtolower( trim( $key, " \t*-\r\n" ) );
138+
$value = trim( $value, " \t*-\r\n" );
139+
140+
if ( $only_valid && ! isset( $this->valid_headers[ $key ] ) ) {
141+
return false;
142+
}
143+
144+
return array( $key, $value );
145+
}
146+
147+
/**
148+
* Sanitizes the Tested header to ensure that it's a valid version header.
149+
*
150+
* @param string $version
151+
* @return string The sanitized $version
152+
*/
153+
private function sanitize_tested_version( $version ) {
154+
$version = trim( $version );
155+
156+
if ( $version ) {
157+
158+
// Handle the edge-case of 'WordPress 5.0' and 'WP 5.0' for historical purposes.
159+
$strip_phrases = [
160+
'WordPress',
161+
'WP',
162+
];
163+
$version = trim( str_ireplace( $strip_phrases, '', $version ) );
164+
165+
// Strip off any -alpha, -RC, -beta suffixes, as these complicate comparisons and are rarely used.
166+
list( $version, ) = explode( '-', $version );
167+
}
168+
169+
return $version;
170+
}
171+
172+
/**
173+
* @param string $line
174+
* @return string
175+
*/
176+
private function strip_newlines( $line ) {
177+
return rtrim( $line, "\r\n" );
178+
}
179+
180+
/**
181+
* @param array $contents
182+
* @return string
183+
*/
184+
private function get_first_nonwhitespace( &$contents ) {
185+
$line = array_shift( $contents );
186+
187+
while ( null !== $line ) {
188+
$trimmed = trim( $line );
189+
190+
if ( ! empty( $trimmed ) ) {
191+
break;
192+
}
193+
194+
$line = array_shift( $contents );
195+
}
196+
197+
return $line ? $line : '';
198+
}
199+
}

0 commit comments

Comments
 (0)