WordPress.org

Make WordPress Core

Ticket #43992: 43992.13.diff

File 43992.13.diff, 16.2 KB (added by afragen, 22 months ago)

improve sanitization and docblocks

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

    diff --git src/wp-admin/includes/plugin.php src/wp-admin/includes/plugin.php
    index 05e3861f17..28e22e5259 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        require_once ABSPATH . '/wp-admin/includes/class-readme-header-parser.php';
     238        $parser      = new WP_Readme_Header_Parser();
     239        $plugin_data = null;
     240        $readme_file = WP_PLUGIN_DIR . '/' . dirname( $plugin_file ) . '/readme.txt';
     241        if ( file_exists( $readme_file ) ) {
     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     = $parser->sanitize_requires_version( $plugin_headers['RequiresWP'] );
     254                $plugin_data->requires_php = $parser->sanitize_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}