WordPress.org

Make WordPress Core

Ticket #43992: 43992.12.diff

File 43992.12.diff, 16.8 KB (added by afragen, 20 months ago)

now based on new local readme parser

  • new file src/wp-admin/includes/class-readme-header-parser.php

    diff --git src/wp-admin/includes/class-readme-header-parser.php src/wp-admin/includes/class-readme-header-parser.php
    new file mode 100644
    index 0000000000..ed1e172359
    - +  
     1<?php
     2/**
     3 * Based on WordPress.org Plugin Readme Parser.
     4 *
     5 * Parses a local `readme.txt` file returning the sanitized headers.
     6 *
     7 * @since 5.2.0
     8 */
     9class WP_Readme_Header_Parser {
     10
     11        /**
     12         * @var string
     13         */
     14        public $name = '';
     15
     16        /**
     17         * @var array
     18         */
     19        public $tags = array();
     20
     21        /**
     22         * @var string
     23         */
     24        public $requires = '';
     25
     26        /**
     27         * @var string
     28         */
     29        public $tested = '';
     30
     31        /**
     32         * @var string
     33         */
     34        public $requires_php = '';
     35
     36        /**
     37         * @var array
     38         */
     39        public $contributors = array();
     40
     41        /**
     42         * @var string
     43         */
     44        public $stable_tag = '';
     45
     46        /**
     47         * @var string
     48         */
     49        public $donate_link = '';
     50
     51        /**
     52         * @var string
     53         */
     54        public $short_description = '';
     55
     56        /**
     57         * @var string
     58         */
     59        public $license = '';
     60
     61        /**
     62         * @var string
     63         */
     64        public $license_uri = '';
     65
     66        /**
     67         * These are the valid header mappings for the header.
     68         *
     69         * @var array
     70         */
     71        private $valid_headers = array(
     72                'tested'            => 'tested',
     73                'tested up to'      => 'tested',
     74                'requires'          => 'requires',
     75                'requires at least' => 'requires',
     76                'requires php'      => 'requires_php',
     77                'tags'              => 'tags',
     78                'contributors'      => 'contributors',
     79                'donate link'       => 'donate_link',
     80                'stable tag'        => 'stable_tag',
     81                'license'           => 'license',
     82                'license uri'       => 'license_uri',
     83        );
     84
     85        /**
     86         * These plugin tags are ignored.
     87         *
     88         * @var array
     89         */
     90        private $ignore_tags = array(
     91                'plugin',
     92                'wordpress',
     93        );
     94
     95        /**
     96         * @param string $file
     97         * @return array $headers
     98         */
     99        public function parse_readme( $file ) {
     100                $contents = file_get_contents( $file );
     101                if ( preg_match( '!!u', $contents ) ) {
     102                        $contents = preg_split( '!\R!u', $contents );
     103                } else {
     104                        $contents = preg_split( '!\R!', $contents ); // regex failed due to invalid UTF8 in $contents, see #2298
     105                }
     106                $contents = array_map( array( $this, 'strip_newlines' ), $contents );
     107
     108                // Strip UTF8 BOM if present.
     109                if ( 0 === strpos( $contents[0], "\xEF\xBB\xBF" ) ) {
     110                        $contents[0] = substr( $contents[0], 3 );
     111                }
     112
     113                // Convert UTF-16 files.
     114                if ( 0 === strpos( $contents[0], "\xFF\xFE" ) ) {
     115                        foreach ( $contents as $i => $line ) {
     116                                $contents[ $i ] = mb_convert_encoding( $line, 'UTF-8', 'UTF-16' );
     117                        }
     118                }
     119
     120                $line       = $this->get_first_nonwhitespace( $contents );
     121                $this->name = $this->sanitize_text( trim( $line, "#= \t\0\x0B" ) );
     122
     123                // Strip Github style header\n==== underlines.
     124                if ( ! empty( $contents ) && '' === trim( $contents[0], '=-' ) ) {
     125                        array_shift( $contents );
     126                }
     127
     128                // Handle readme's which do `=== Plugin Name ===\nMy SuperAwesomePlugin Name\n...`
     129                if ( 'plugin name' == strtolower( $this->name ) ) {
     130                        $this->name = $line = $this->get_first_nonwhitespace( $contents );
     131
     132                        // Ensure that the line read wasn't an actual header or description.
     133                        if ( strlen( $line ) > 50 || preg_match( '~^(' . implode( '|', array_keys( $this->valid_headers ) ) . ')\s*:~i', $line ) ) {
     134                                $this->name = false;
     135                                array_unshift( $contents, $line );
     136                        }
     137                }
     138
     139                // Parse headers.
     140                $headers = array();
     141
     142                $line = $this->get_first_nonwhitespace( $contents );
     143                do {
     144                        $value = null;
     145                        if ( false === strpos( $line, ':' ) ) {
     146
     147                                // Some plugins have line-breaks within the headers.
     148                                if ( empty( $line ) ) {
     149                                        break;
     150                                } else {
     151                                        continue;
     152                                }
     153                        }
     154
     155                        $bits                = explode( ':', trim( $line ), 2 );
     156                        list( $key, $value ) = $bits;
     157                        $key                 = strtolower( trim( $key, " \t*-\r\n" ) );
     158                        if ( isset( $this->valid_headers[ $key ] ) ) {
     159                                $headers[ $this->valid_headers[ $key ] ] = trim( $value );
     160                        }
     161                } while ( ( $line = array_shift( $contents ) ) !== null );
     162                array_unshift( $contents, $line );
     163
     164                if ( ! empty( $headers['tags'] ) ) {
     165                        $this->tags = explode( ',', $headers['tags'] );
     166                        $this->tags = array_map( 'trim', $this->tags );
     167                        $this->tags = array_filter( $this->tags );
     168                        $this->tags = array_diff( $this->tags, $this->ignore_tags );
     169                        $this->tags = array_slice( $this->tags, 0, 5 );
     170                }
     171                if ( ! empty( $headers['requires'] ) ) {
     172                        $this->requires = $this->sanitize_requires_version( $headers['requires'] );
     173                }
     174                if ( ! empty( $headers['tested'] ) ) {
     175                        $this->tested = $this->sanitize_tested_version( $headers['tested'] );
     176                }
     177                if ( ! empty( $headers['requires_php'] ) ) {
     178                        $this->requires_php = $this->sanitize_requires_php( $headers['requires_php'] );
     179                }
     180                if ( ! empty( $headers['contributors'] ) ) {
     181                        $this->contributors = explode( ',', $headers['contributors'] );
     182                        $this->contributors = array_map( 'trim', $this->contributors );
     183                        $this->contributors = $this->sanitize_contributors( $this->contributors );
     184                }
     185                if ( ! empty( $headers['stable_tag'] ) ) {
     186                        $this->stable_tag = $this->sanitize_stable_tag( $headers['stable_tag'] );
     187                }
     188                if ( ! empty( $headers['donate_link'] ) ) {
     189                        $this->donate_link = $headers['donate_link'];
     190                }
     191                if ( ! empty( $headers['license'] ) ) {
     192                        // Handle the many cases of "License: GPLv2 - http://..."
     193                        if ( empty( $headers['license_uri'] ) && preg_match( '!(https?://\S+)!i', $headers['license'], $url ) ) {
     194                                $headers['license_uri'] = $url[1];
     195                                $headers['license']     = trim( str_replace( $url[1], '', $headers['license'] ), " -*\t\n\r\n" );
     196                        }
     197                        $this->license = $headers['license'];
     198                }
     199                if ( ! empty( $headers['license_uri'] ) ) {
     200                        $this->license_uri = $headers['license_uri'];
     201                }
     202
     203                return $headers;
     204        }
     205
     206        /**
     207         * @access protected
     208         *
     209         * @param string $contents
     210         * @return string
     211         */
     212        protected function get_first_nonwhitespace( &$contents ) {
     213                while ( ( $line = array_shift( $contents ) ) !== null ) {
     214                        $trimmed = trim( $line );
     215                        if ( ! empty( $trimmed ) ) {
     216                                break;
     217                        }
     218                }
     219
     220                return $line;
     221        }
     222
     223        /**
     224         * @access protected
     225         *
     226         * @param string $line
     227         * @return string
     228         */
     229        protected function strip_newlines( $line ) {
     230                return rtrim( $line, "\r\n" );
     231        }
     232
     233        /**
     234         * @access protected
     235         *
     236         * @param string $text
     237         * @return string
     238         */
     239        protected function sanitize_text( $text ) {
     240                // not fancy
     241                $text = strip_tags( $text );
     242                $text = esc_html( $text );
     243                $text = trim( $text );
     244
     245                return $text;
     246        }
     247
     248        /**
     249         * Sanitize provided contributors to valid WordPress users
     250         *
     251         * @param array $users Array of user_login's or user_nicename's.
     252         * @return array Array of user_logins.
     253         */
     254        protected function sanitize_contributors( $users ) {
     255                foreach ( $users as $i => $name ) {
     256                        // Contributors should be listed by their WordPress.org Login name (Example: 'Joe Bloggs')
     257                        $user = get_user_by( 'login', $name );
     258
     259                        // Or failing that, by their user_nicename field (Example: 'joe-bloggs')
     260                        if ( ! $user ) {
     261                                $user = get_user_by( 'slug', $name );
     262                        }
     263
     264                        // In the event that something invalid is used, we'll ignore it (Example: 'Joe Bloggs (Australian Translation)')
     265                        if ( ! $user ) {
     266                                unset( $users[ $i ] );
     267                                $this->warnings['contributor_ignored'] = true;
     268                                continue;
     269                        }
     270
     271                        // Overwrite whatever the author has specified with the sanitized nicename.
     272                        $users[ $i ] = $user->user_nicename;
     273                }
     274
     275                return $users;
     276        }
     277
     278        /**
     279         * Sanitize the provided stable tag to something we expect.
     280         *
     281         * @param string $stable_tag the raw Stable Tag line from the readme.
     282         * @return string The sanitized $stable_tag.
     283         */
     284        protected function sanitize_stable_tag( $stable_tag ) {
     285                $stable_tag = trim( $stable_tag );
     286                $stable_tag = trim( $stable_tag, '"\'' ); // "trunk"
     287                $stable_tag = preg_replace( '!^/?tags/!i', '', $stable_tag ); // "tags/1.2.3"
     288                $stable_tag = preg_replace( '![^a-z0-9_.-]!i', '', $stable_tag );
     289
     290                // If the stable_tag begins with a ., we treat it as 0.blah.
     291                if ( '.' == substr( $stable_tag, 0, 1 ) ) {
     292                        $stable_tag = "0{$stable_tag}";
     293                }
     294
     295                return $stable_tag;
     296        }
     297
     298        /**
     299         * Sanitizes the Requires PHP header to ensure that it's a valid version header.
     300         *
     301         * @param string $version
     302         * @return string The sanitized $version
     303         */
     304        protected function sanitize_requires_php( $version ) {
     305                $version = trim( $version );
     306
     307                // x.y or x.y.z
     308                if ( $version && ! preg_match( '!^\d+(\.\d+){1,2}$!', $version ) ) {
     309                        $this->warnings['requires_php_header_ignored'] = true;
     310                        // Ignore the readme value.
     311                        $version = '';
     312                }
     313
     314                return $version;
     315        }
     316
     317        /**
     318         * Sanitizes the Tested header to ensure that it's a valid version header.
     319         *
     320         * @param string $version
     321         * @return string The sanitized $version
     322         */
     323        protected function sanitize_tested_version( $version ) {
     324                $version = trim( $version );
     325
     326                if ( $version ) {
     327
     328                        // Handle the edge-case of 'WordPress 5.0' and 'WP 5.0' for historical purposes.
     329                        $strip_phrases = array(
     330                                'WordPress',
     331                                'WP',
     332                        );
     333                        $version       = trim( str_ireplace( $strip_phrases, '', $version ) );
     334
     335                        // Strip off any -alpha, -RC, -beta suffixes, as these complicate comparisons and are rarely used.
     336                        list( $version, ) = explode( '-', $version );
     337
     338                        if (
     339                                // x.y or x.y.z
     340                                ! preg_match( '!^\d+\.\d(\.\d+)?$!', $version ) ||
     341                                // Allow plugins to mark themselves as compatible with Stable+0.1 (trunk/master) but not higher
     342                                defined( 'WP_CORE_STABLE_BRANCH' ) && ( (float) $version > (float) WP_CORE_STABLE_BRANCH + 0.1 )
     343                        ) {
     344                                $this->warnings['tested_header_ignored'] = true;
     345                                // Ignore the readme value.
     346                                $version = '';
     347                        }
     348                }
     349
     350                return $version;
     351        }
     352
     353        /**
     354         * Sanitizes the Requires at least header to ensure that it's a valid version header.
     355         *
     356         * @param string $version
     357         * @return string The sanitized $version
     358         */
     359        protected function sanitize_requires_version( $version ) {
     360                $version = trim( $version );
     361
     362                if ( $version ) {
     363
     364                        // Handle the edge-case of 'WordPress 5.0' and 'WP 5.0' for historical purposes.
     365                        $strip_phrases = array(
     366                                'WordPress',
     367                                'WP',
     368                                'or higher',
     369                                'and above',
     370                                '+',
     371                        );
     372                        $version       = trim( str_ireplace( $strip_phrases, '', $version ) );
     373
     374                        // Strip off any -alpha, -RC, -beta suffixes, as these complicate comparisons and are rarely used.
     375                        list( $version, ) = explode( '-', $version );
     376
     377                        if (
     378                                // x.y or x.y.z
     379                                ! preg_match( '!^\d+\.\d(\.\d+)?$!', $version ) ||
     380                                // Allow plugins to mark themselves as requireing Stable+0.1 (trunk/master) but not higher
     381                                defined( 'WP_CORE_STABLE_BRANCH' ) && ( (float) $version > (float) WP_CORE_STABLE_BRANCH + 0.1 )
     382                        ) {
     383                                $this->warnings['requires_header_ignored'] = true;
     384                                // Ignore the readme value.
     385                                $version = '';
     386                        }
     387                }
     388
     389                return $version;
     390        }
     391}
  • src/wp-admin/includes/plugin.php

    diff --git src/wp-admin/includes/plugin.php src/wp-admin/includes/plugin.php
    index 05e3861f17..3ac2b94d93 100644
     
    3131 *     Network: Optional. Specify "Network: true" to require that a plugin is activated
    3232 *          across all sites in an installation. This will prevent a plugin from being
    3333 *          activated on a single site when Multisite is enabled.
     34 *     Requires WP: Optional. Specify the minimum required WordPress version.
     35 *     Requires PHP: Optional. Specify the minimum required PHP version.
    3436 *      * / # Remove the space to close comment
    3537 *
    3638 * Some users have issues with opening large files and manipulating the contents
     
    4648 * reading.
    4749 *
    4850 * @since 1.5.0
     51 * @since 5.2.0 Added `RequiresWP` and `RequiresPHP`.
    4952 *
    5053 * @param string $plugin_file Absolute path to the main plugin file.
    5154 * @param bool   $markup      Optional. If the returned data should have HTML markup applied.
     
    6366 *     @type string $TextDomain  Plugin textdomain.
    6467 *     @type string $DomainPath  Plugins relative directory path to .mo files.
    6568 *     @type bool   $Network     Whether the plugin can only be activated network-wide.
     69 *     @type string $RequiresWP  Minimum required version of WordPress.
     70 *     @type string $RequiresPHP Minimum required version of PHP.
    6671 * }
    6772 */
    6873function get_plugin_data( $plugin_file, $markup = true, $translate = true ) {
    function get_plugin_data( $plugin_file, $markup = true, $translate = true ) { 
    7782                'TextDomain'  => 'Text Domain',
    7883                'DomainPath'  => 'Domain Path',
    7984                'Network'     => 'Network',
     85                'RequiresWP'  => 'Requires WP',
     86                'RequiresPHP' => 'Requires PHP',
    8087                // Site Wide Only is deprecated in favor of Network.
    8188                '_sitewide'   => 'Site Wide Only',
    8289        );
    function _get_plugin_data_markup_translate( $plugin_file, $plugin_data, $markup 
    213220        return $plugin_data;
    214221}
    215222
     223/**
     224 * Get and return plugin data used for validation.
     225 *
     226 * @uses `wp-admin/includes/class-readme-header-parser.php to parse local `readme.txt`.
     227 * Alternately see if a plugin header `Requires WP` or `Requires PHP` exists and use that.
     228 *
     229 * @since 5.2.0
     230 * @see validate_plugin_requirements()
     231 *
     232 * @param string $plugin_file Path to the plugin file relative to the plugins directory.
     233 *
     234 * @return object $plugin_data Object of plugin data for validation.
     235 */
     236function get_plugin_validation_data( $plugin_file ) {
     237        $plugin_data = null;
     238        require_once ABSPATH . '/wp-admin/includes/class-readme-header-parser.php';
     239        $readme_file = WP_PLUGIN_DIR . '/' . dirname( $plugin_file ) . '/readme.txt';
     240        if ( file_exists( $readme_file ) ) {
     241                $parser      = new WP_Readme_Header_Parser();
     242                $plugin_data = (object) $parser->parse_readme( $readme_file );
     243        }
     244
     245        /*
     246         * Plugin has no `readme.txt` file but might have
     247         * `Requires WP` and/or `Requires PHP` headers we can use.
     248         */
     249        if ( null === $plugin_data ) {
     250                $plugin_data               = new stdClass();
     251                $plugin_data->file         = $plugin_file;
     252                $plugin_headers            = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin_file );
     253                $plugin_data->requires     = $plugin_headers['RequiresWP'];
     254                $plugin_data->requires_php = $plugin_headers['RequiresPHP'];
     255        }
     256
     257        return $plugin_data;
     258}
     259
    216260/**
    217261 * Get a list of a plugin's files.
    218262 *
    function is_network_only_plugin( $plugin ) { 
    675719 * ensure that the success redirection will update the error redirection.
    676720 *
    677721 * @since 2.5.0
     722 * @since 5.2.0 Test for WordPress version and PHP version compatibility.
    678723 *
    679724 * @param string $plugin       Path to the plugin file relative to the plugins directory.
    680725 * @param string $redirect     Optional. URL to redirect to.
    function activate_plugin( $plugin, $redirect = '', $network_wide = false, $silen 
    699744                return $valid;
    700745        }
    701746
     747        if ( ! validate_plugin_requirements( $plugin ) ) {
     748                return new WP_Error( 'plugin_activation_error', __( 'Plugin does not meet minimum WordPress and/or PHP requirements.' ) );
     749        }
     750
    702751        if ( ( $network_wide && ! isset( $current[ $plugin ] ) ) || ( ! $network_wide && ! in_array( $plugin, $current ) ) ) {
    703752                if ( ! empty( $redirect ) ) {
    704753                        wp_redirect( add_query_arg( '_error_nonce', wp_create_nonce( 'plugin-activation-error_' . $plugin ), $redirect ) ); // we'll override this later if the plugin can be included without fatal error
    function validate_plugin( $plugin ) { 
    11991248        return 0;
    12001249}
    12011250
     1251/**
     1252 * Validate the plugin requirements for WP version and PHP version.
     1253 *
     1254 * @since 5.2.0
     1255 * @see activate_plugin()
     1256 *
     1257 * @param string $plugin Path to the plugin file relative to the plugins directory.
     1258 *
     1259 * @return bool Default to true and if requirements met, false if not.
     1260 */
     1261function validate_plugin_requirements( $plugin ) {
     1262        $plugin_data  = get_plugin_validation_data( $plugin );
     1263        $wp_requires  = isset( $plugin_data->requires ) ? $plugin_data->requires : null;
     1264        $php_requires = isset( $plugin_data->requires_php ) ? $plugin_data->requires_php : null;
     1265
     1266        return is_wp_compatible( $wp_requires ) && is_php_compatible( $php_requires );
     1267}
     1268
    12021269/**
    12031270 * Whether the plugin can be uninstalled.
    12041271 *
  • src/wp-includes/functions.php

    diff --git src/wp-includes/functions.php src/wp-includes/functions.php
    index 214c134d01..b25bc6e74a 100644
    function wp_update_php_annotation() { 
    68306830        );
    68316831        echo'</p>';
    68326832}
     6833
     6834/**
     6835 * Check compatibility with current WordPress version.
     6836 *
     6837 * @since 5.2.0
     6838 *
     6839 * @param string $requires Minimum WordPress version from API.
     6840 *
     6841 * @return bool True if is compatible or empty, false if not.
     6842 */
     6843function is_wp_compatible( $requires ) {
     6844        $wp_version = get_bloginfo( 'version' );
     6845        return empty( $requires ) || version_compare( $wp_version, $requires, '>=' );
     6846}
     6847
     6848/**
     6849 * Check compatibility with current PHP version.
     6850 *
     6851 * @since 5.2.0
     6852 *
     6853 * @param string $requires Minimum PHP version from API.
     6854 *
     6855 * @return bool True if is compatible or empty, false if not.
     6856 */
     6857function is_php_compatible( $requires ) {
     6858        return empty( $requires ) || version_compare( phpversion(), $requires, '>=' );
     6859}