From a1789ddde42033f1b05cc4929491214ee6e79383 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Thu, 17 Dec 2015 09:15:42 +0100 Subject: Update to MediaWiki 1.26.0 --- includes/utils/AutoloadGenerator.php | 74 +++++- includes/utils/AvroValidator.php | 184 ++++++++++++++ includes/utils/BatchRowIterator.php | 278 ++++++++++++++++++++++ includes/utils/BatchRowUpdate.php | 133 +++++++++++ includes/utils/BatchRowWriter.php | 71 ++++++ includes/utils/IP.php | 6 +- includes/utils/MWCryptHKDF.php | 8 +- includes/utils/MWCryptRand.php | 8 +- includes/utils/MWFunction.php | 40 ---- includes/utils/RowUpdateGenerator.php | 39 +++ includes/utils/UIDGenerator.php | 21 +- includes/utils/iterators/IteratorDecorator.php | 50 ++++ includes/utils/iterators/NotRecursiveIterator.php | 35 +++ 13 files changed, 881 insertions(+), 66 deletions(-) create mode 100644 includes/utils/AvroValidator.php create mode 100644 includes/utils/BatchRowIterator.php create mode 100644 includes/utils/BatchRowUpdate.php create mode 100644 includes/utils/BatchRowWriter.php delete mode 100644 includes/utils/MWFunction.php create mode 100644 includes/utils/RowUpdateGenerator.php create mode 100644 includes/utils/iterators/IteratorDecorator.php create mode 100644 includes/utils/iterators/NotRecursiveIterator.php (limited to 'includes/utils') diff --git a/includes/utils/AutoloadGenerator.php b/includes/utils/AutoloadGenerator.php index 9cf8cab5..7d631563 100644 --- a/includes/utils/AutoloadGenerator.php +++ b/includes/utils/AutoloadGenerator.php @@ -119,13 +119,49 @@ class AutoloadGenerator { } /** - * Write out all known classes to autoload.php in - * the provided basedir + * Updates the AutoloadClasses field at the given + * filename. * - * @param string $commandName Value used in file comment to direct - * developers towards the appropriate way to update the autoload. + * @param {string} $filename Filename of JSON + * extension/skin registration file */ - public function generateAutoload( $commandName = 'AutoloadGenerator' ) { + protected function generateJsonAutoload( $filename ) { + require_once __DIR__ . '/../../includes/json/FormatJson.php'; + $key = 'AutoloadClasses'; + $json = FormatJson::decode( file_get_contents( $filename ), true ); + unset( $json[$key] ); + // Inverting the key-value pairs so that they become of the + // format class-name : path when they get converted into json. + foreach ( $this->classes as $path => $contained ) { + foreach ( $contained as $fqcn ) { + + // Using substr to remove the leading '/' + $json[$key][$fqcn] = substr( $path, 1 ); + } + } + foreach ( $this->overrides as $path => $fqcn ) { + + // Using substr to remove the leading '/' + $json[$key][$fqcn] = substr( $path, 1 ); + } + + // Sorting the list of autoload classes. + ksort( $json[$key] ); + + // Update file, using constants for the required + // formatting. + file_put_contents( $filename, + FormatJson::encode( $json, true ) . "\n" ); + } + + /** + * Generates a PHP file setting up autoload information. + * + * @param {string} $commandName Command name to include in comment + * @param {string} $filename of PHP file to put autoload information in. + */ + protected function generatePHPAutoload( $commandName, $filename ) { + // No existing JSON file found; update/generate PHP file $content = array(); // We need to generate a line each rather than exporting the @@ -163,7 +199,7 @@ class AutoloadGenerator { $output = implode( "\n\t", $content ); file_put_contents( - $this->basepath . '/autoload.php', + $filename, <<variableName}; EOD ); + } + /** + * Write out all known classes to autoload.php, extension.json, or skin.json in + * the provided basedir + * + * @param string $commandName Value used in file comment to direct + * developers towards the appropriate way to update the autoload. + */ + public function generateAutoload( $commandName = 'AutoloadGenerator' ) { + + // We need to check whether an extenson.json or skin.json exists or not, and + // incase it doesn't, update the autoload.php file. + + $jsonFilename = null; + if ( file_exists( $this->basepath . "/extension.json" ) ) { + $jsonFilename = $this->basepath . "/extension.json"; + } elseif ( file_exists( $this->basepath . "/skin.json" ) ) { + $jsonFilename = $this->basepath . "/skin.json"; + } + + if ( $jsonFilename !== null ) { + $this->generateJsonAutoload( $jsonFilename ); + } else { + $this->generatePHPAutoload( $commandName, $this->basepath . '/autoload.php' ); + } + } /** * Ensure that Unix-style path separators ("/") are used in the path. * diff --git a/includes/utils/AvroValidator.php b/includes/utils/AvroValidator.php new file mode 100644 index 00000000..4f8e0b17 --- /dev/null +++ b/includes/utils/AvroValidator.php @@ -0,0 +1,184 @@ + + * @copyright © 2015 Erik Bernhardson and Wikimedia Foundation. + */ +class AvroValidator { + /** + * @param AvroSchema $schema The rules to conform to. + * @param mixed $datum The value to validate against $schema. + * @return string|string[] An error or list of errors in the + * provided $datum. When no errors exist the empty array is + * returned. + */ + public static function getErrors( AvroSchema $schema, $datum ) { + switch ( $schema->type) { + case AvroSchema::NULL_TYPE: + if ( !is_null($datum) ) { + return self::wrongType( 'null', $datum ); + } + return array(); + case AvroSchema::BOOLEAN_TYPE: + if ( !is_bool($datum) ) { + return self::wrongType( 'boolean', $datum ); + } + return array(); + case AvroSchema::STRING_TYPE: + case AvroSchema::BYTES_TYPE: + if ( !is_string($datum) ) { + return self::wrongType( 'string', $datum ); + } + return array(); + case AvroSchema::INT_TYPE: + if ( !is_int($datum) ) { + return self::wrongType( 'integer', $datum ); + } + if ( AvroSchema::INT_MIN_VALUE > $datum + || $datum > AvroSchema::INT_MAX_VALUE + ) { + return self::outOfRange( + AvroSchema::INT_MIN_VALUE, + AvroSchema::INT_MAX_VALUE, + $datum + ); + } + return array(); + case AvroSchema::LONG_TYPE: + if ( !is_int($datum) ) { + return self::wrongType( 'integer', $datum ); + } + if ( AvroSchema::LONG_MIN_VALUE > $datum + || $datum > AvroSchema::LONG_MAX_VALUE + ) { + return self::outOfRange( + AvroSchema::LONG_MIN_VALUE, + AvroSchema::LONG_MAX_VALUE, + $datum + ); + } + return array(); + case AvroSchema::FLOAT_TYPE: + case AvroSchema::DOUBLE_TYPE: + if ( !is_float($datum) && !is_int($datum) ) { + return self::wrongType( 'float or integer', $datum ); + } + return array(); + case AvroSchema::ARRAY_SCHEMA: + if (!is_array($datum)) { + return self::wrongType( 'array', $datum ); + } + $errors = array(); + foreach ($datum as $d) { + $result = $this->validate( $schema->items(), $d ); + if ( $result ) { + $errors[] = $result; + } + } + if ( $errors ) { + return $errors; + } + return array(); + case AvroSchema::MAP_SCHEMA: + if (!is_array($datum)) { + return self::wrongType( 'array', $datum ); + } + $errors = array(); + foreach ($datum as $k => $v) { + if ( !is_string($k) ) { + $errors[] = self::wrongType( 'string key', $k ); + } + $result = self::getErrors( $schema->values(), $v ); + if ( $result ) { + $errors[$k] = $result; + } + } + return $errors; + case AvroSchema::UNION_SCHEMA: + $errors = array(); + foreach ($schema->schemas() as $schema) { + $result = self::getErrors( $schema, $datum ); + if ( !$result ) { + return array(); + } + $errors[] = $result; + } + if ( $errors ) { + return array( "Expected any one of these to be true", $errors ); + } + return "No schemas provided to union"; + case AvroSchema::ENUM_SCHEMA: + if ( !in_array( $datum, $schema->symbols() ) ) { + $symbols = implode( ', ', $schema->symbols ); + return "Expected one of $symbols but recieved $datum"; + } + return array(); + case AvroSchema::FIXED_SCHEMA: + if ( !is_string( $datum ) ) { + return self::wrongType( 'string', $datum ); + } + $len = strlen( $datum ); + if ( $len !== $schema->size() ) { + return "Expected string of length {$schema->size()}, " + . "but recieved one of length $len"; + } + return array(); + case AvroSchema::RECORD_SCHEMA: + case AvroSchema::ERROR_SCHEMA: + case AvroSchema::REQUEST_SCHEMA: + if ( !is_array( $datum ) ) { + return self::wrongType( 'array', $datum ); + } + $errors = array(); + foreach ( $schema->fields() as $field ) { + $name = $field->name(); + if ( !array_key_exists( $name, $datum ) ) { + $errors[$name] = 'Missing expected field'; + continue; + } + $result = self::getErrors( $field->type(), $datum[$name] ); + if ( $result ) { + $errors[$name] = $result; + } + } + return $errors; + default: + return "Unknown avro schema type: {$schema->type}"; + } + } + + public static function typeOf( $datum ) { + return is_object( $datum ) ? get_class( $datum ) : gettype( $datum ); + } + + public static function wrongType( $expected, $datum ) { + return "Expected $expected, but recieved " . self::typeOf( $datum ); + } + + public static function outOfRange( $min, $max, $datum ) { + return "Expected value between $min and $max, but recieved $datum"; + } +} diff --git a/includes/utils/BatchRowIterator.php b/includes/utils/BatchRowIterator.php new file mode 100644 index 00000000..9441608a --- /dev/null +++ b/includes/utils/BatchRowIterator.php @@ -0,0 +1,278 @@ +primaryKey + */ + protected $orderBy; + + /** + * @var array $current The current iterator value + */ + private $current = array(); + + /** + * @var integer key 0-indexed number of pages fetched since self::reset() + */ + private $key; + + /** + * @param DatabaseBase $db The database to read from + * @param string $table The name of the table to read from + * @param string|array $primaryKey The name or names of the primary key columns + * @param integer $batchSize The number of rows to fetch per iteration + * @throws MWException + */ + public function __construct( DatabaseBase $db, $table, $primaryKey, $batchSize ) { + if ( $batchSize < 1 ) { + throw new MWException( 'Batch size must be at least 1 row.' ); + } + $this->db = $db; + $this->table = $table; + $this->primaryKey = (array) $primaryKey; + $this->fetchColumns = $this->primaryKey; + $this->orderBy = implode( ' ASC,', $this->primaryKey ) . ' ASC'; + $this->batchSize = $batchSize; + } + + /** + * @param array $condition Query conditions suitable for use with + * DatabaseBase::select + */ + public function addConditions( array $conditions ) { + $this->conditions = array_merge( $this->conditions, $conditions ); + } + + /** + * @param array $condition Query join conditions suitable for use + * with DatabaseBase::select + */ + public function addJoinConditions( array $conditions ) { + $this->joinConditions = array_merge( $this->joinConditions, $conditions ); + } + + /** + * @param array $columns List of column names to select from the + * table suitable for use with DatabaseBase::select() + */ + public function setFetchColumns( array $columns ) { + // If it's not the all column selector merge in the primary keys we need + if ( count( $columns ) === 1 && reset( $columns ) === '*' ) { + $this->fetchColumns = $columns; + } else { + $this->fetchColumns = array_unique( array_merge( + $this->primaryKey, + $columns + ) ); + } + } + + /** + * Extracts the primary key(s) from a database row. + * + * @param stdClass $row An individual database row from this iterator + * @return array Map of primary key column to value within the row + */ + public function extractPrimaryKeys( $row ) { + $pk = array(); + foreach ( $this->primaryKey as $column ) { + $pk[$column] = $row->$column; + } + return $pk; + } + + /** + * @return array The most recently fetched set of rows from the database + */ + public function current() { + return $this->current; + } + + /** + * @return integer 0-indexed count of the page number fetched + */ + public function key() { + return $this->key; + } + + /** + * Reset the iterator to the begining of the table. + */ + public function rewind() { + $this->key = -1; // self::next() will turn this into 0 + $this->current = array(); + $this->next(); + } + + /** + * @return boolean True when the iterator is in a valid state + */ + public function valid() { + return (bool) $this->current; + } + + /** + * @return boolean True when this result set has rows + */ + public function hasChildren() { + return $this->current && count( $this->current ); + } + + /** + * @return RecursiveIterator + */ + public function getChildren() { + return new NotRecursiveIterator( new ArrayIterator( $this->current ) ); + } + + /** + * Fetch the next set of rows from the database. + */ + public function next() { + $res = $this->db->select( + $this->table, + $this->fetchColumns, + $this->buildConditions(), + __METHOD__, + array( + 'LIMIT' => $this->batchSize, + 'ORDER BY' => $this->orderBy, + ), + $this->joinConditions + ); + + // The iterator is converted to an array because in addition to + // returning it in self::current() we need to use the end value + // in self::buildConditions() + $this->current = iterator_to_array( $res ); + $this->key++; + } + + /** + * Uses the primary key list and the maximal result row from the + * previous iteration to build an SQL condition sufficient for + * selecting the next page of results. All except the final key use + * `=` conditions while the final key uses a `>` condition + * + * Example output: + * array( '( foo = 42 AND bar > 7 ) OR ( foo > 42 )' ) + * + * @return array The SQL conditions necessary to select the next set + * of rows in the batched query + */ + protected function buildConditions() { + if ( !$this->current ) { + return $this->conditions; + } + + $maxRow = end( $this->current ); + $maximumValues = array(); + foreach ( $this->primaryKey as $column ) { + $maximumValues[$column] = $this->db->addQuotes( $maxRow->$column ); + } + + $pkConditions = array(); + // For example: If we have 3 primary keys + // first run through will generate + // col1 = 4 AND col2 = 7 AND col3 > 1 + // second run through will generate + // col1 = 4 AND col2 > 7 + // and the final run through will generate + // col1 > 4 + while ( $maximumValues ) { + $pkConditions[] = $this->buildGreaterThanCondition( $maximumValues ); + array_pop( $maximumValues ); + } + + $conditions = $this->conditions; + $conditions[] = sprintf( '( %s )', implode( ' ) OR ( ', $pkConditions ) ); + + return $conditions; + } + + /** + * Given an array of column names and their maximum value generate + * an SQL condition where all keys except the last match $quotedMaximumValues + * exactly and the last column is greater than the matching value in + * $quotedMaximumValues + * + * @param array $quotedMaximumValues The maximum values quoted with + * $this->db->addQuotes() + * @return string An SQL condition that will select rows where all + * columns match the maximum value exactly except the last column + * which must be greater than the provided maximum value + */ + protected function buildGreaterThanCondition( array $quotedMaximumValues ) { + $keys = array_keys( $quotedMaximumValues ); + $lastColumn = end( $keys ); + $lastValue = array_pop( $quotedMaximumValues ); + $conditions = array(); + foreach ( $quotedMaximumValues as $column => $value ) { + $conditions[] = "$column = $value"; + } + $conditions[] = "$lastColumn > $lastValue"; + + return implode( ' AND ', $conditions ); + } +} diff --git a/includes/utils/BatchRowUpdate.php b/includes/utils/BatchRowUpdate.php new file mode 100644 index 00000000..a4257a53 --- /dev/null +++ b/includes/utils/BatchRowUpdate.php @@ -0,0 +1,133 @@ +execute(); + * + * An example maintenance script utilizing the BatchRowUpdate can be + * located in the Echo extension file maintenance/updateSchema.php + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup Maintenance + */ +class BatchRowUpdate { + /** + * @var BatchRowIterator $reader Iterator that returns an array of + * database rows + */ + protected $reader; + + /** + * @var BatchRowWriter $writer Writer capable of pushing row updates + * to the database + */ + protected $writer; + + /** + * @var RowUpdateGenerator $generator Generates single row updates + * based on the rows content + */ + protected $generator; + + /** + * @var callable $output Output callback + */ + protected $output; + + /** + * @param BatchRowIterator $reader Iterator that returns an + * array of database rows + * @param BatchRowWriter $writer Writer capable of pushing + * row updates to the database + * @param RowUpdateGenerator $generator Generates single row updates + * based on the rows content + */ + public function __construct( BatchRowIterator $reader, BatchRowWriter $writer, RowUpdateGenerator $generator ) { + $this->reader = $reader; + $this->writer = $writer; + $this->generator = $generator; + $this->output = function() { + }; // nop + } + + /** + * Runs the batch update process + */ + public function execute() { + foreach ( $this->reader as $rows ) { + $updates = array(); + foreach ( $rows as $row ) { + $update = $this->generator->update( $row ); + if ( $update ) { + $updates[] = array( + 'primaryKey' => $this->reader->extractPrimaryKeys( $row ), + 'changes' => $update, + ); + } + } + + if ( $updates ) { + $this->output( "Processing " . count( $updates ) . " rows\n" ); + $this->writer->write( $updates ); + } + } + + $this->output( "Completed\n" ); + } + + /** + * Accepts a callable which will receive a single parameter + * containing string status updates + * + * @param callable $output A callback taking a single string + * parameter to output + * + * @throws MWException + */ + public function setOutput( $output ) { + if ( !is_callable( $output ) ) { + throw new MWException( + 'Provided $output param is required to be callable.' + ); + } + $this->output = $output; + } + + /** + * Write out a status update + * + * @param string $text The value to print + */ + protected function output( $text ) { + call_user_func( $this->output, $text ); + } +} diff --git a/includes/utils/BatchRowWriter.php b/includes/utils/BatchRowWriter.php new file mode 100644 index 00000000..04c00a3d --- /dev/null +++ b/includes/utils/BatchRowWriter.php @@ -0,0 +1,71 @@ +db = $db; + $this->table = $table; + $this->clusterName = $clusterName; + } + + /** + * @param array $updates Array of arrays each containing two keys, 'primaryKey' + * and 'changes'. primaryKey must contain a map of column names to values + * sufficient to uniquely identify the row changes must contain a map of column + * names to update values to apply to the row. + */ + public function write( array $updates ) { + $this->db->begin(); + + foreach ( $updates as $update ) { + $this->db->update( + $this->table, + $update['changes'], + $update['primaryKey'], + __METHOD__ + ); + } + + $this->db->commit(); + wfWaitForSlaves( false, false, $this->clusterName ); + } +} diff --git a/includes/utils/IP.php b/includes/utils/IP.php index 4441236d..666660aa 100644 --- a/includes/utils/IP.php +++ b/includes/utils/IP.php @@ -21,6 +21,8 @@ * @author Antoine Musso "", Aaron Schulz */ +use IPSet\IPSet; + // Some regex definition to "play" with IP address and IP address blocks // An IPv4 address is made of 4 bytes from x00 to xFF which is d0 to d255 @@ -240,7 +242,7 @@ class IP { * A bare IPv6 address is accepted despite the lack of square brackets. * * @param string $both The string with the host and port - * @return array + * @return array|false Array normally, false on certain failures */ public static function splitHostAndPort( $both ) { if ( substr( $both, 0, 1 ) === '[' ) { @@ -375,6 +377,8 @@ class IP { '127.0.0.0/8', # loopback 'fc00::/7', # RFC 4193 (local) '0:0:0:0:0:0:0:1', # loopback + '169.254.0.0/16', # link-local + 'fe80::/10', # link-local ) ); } return !$privateSet->match( $ip ); diff --git a/includes/utils/MWCryptHKDF.php b/includes/utils/MWCryptHKDF.php index 950dd846..740df922 100644 --- a/includes/utils/MWCryptHKDF.php +++ b/includes/utils/MWCryptHKDF.php @@ -161,7 +161,7 @@ class MWCryptHKDF { * @throws MWException */ protected static function singleton() { - global $wgHKDFAlgorithm, $wgHKDFSecret, $wgSecretKey; + global $wgHKDFAlgorithm, $wgHKDFSecret, $wgSecretKey, $wgMainCacheType; $secret = $wgHKDFSecret ?: $wgSecretKey; if ( !$secret ) { @@ -176,11 +176,7 @@ class MWCryptHKDF { $context[] = gethostname(); // Setup salt cache. Use APC, or fallback to the main cache if it isn't setup - try { - $cache = ObjectCache::newAccelerator( array() ); - } catch ( Exception $e ) { - $cache = wfGetMainCache(); - } + $cache = ObjectCache::newAccelerator( $wgMainCacheType ); if ( is_null( self::$singleton ) ) { self::$singleton = new self( $secret, $wgHKDFAlgorithm, $cache, $context ); diff --git a/includes/utils/MWCryptRand.php b/includes/utils/MWCryptRand.php index e6c0e784..f2237909 100644 --- a/includes/utils/MWCryptRand.php +++ b/includes/utils/MWCryptRand.php @@ -96,9 +96,9 @@ class MWCryptRand { } foreach ( $files as $file ) { - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $stat = stat( $file ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( $stat ) { // stat() duplicates data into numeric and string keys so kill off all the numeric ones foreach ( $stat as $k => $v ) { @@ -363,9 +363,9 @@ class MWCryptRand { } // /dev/urandom is generally considered the best possible commonly // available random source, and is available on most *nix systems. - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $urandom = fopen( "/dev/urandom", "rb" ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); // Attempt to read all our random data from urandom // php's fread always does buffered reads based on the stream's chunk_size diff --git a/includes/utils/MWFunction.php b/includes/utils/MWFunction.php deleted file mode 100644 index fa7eebe8..00000000 --- a/includes/utils/MWFunction.php +++ /dev/null @@ -1,40 +0,0 @@ - $class, - 'args' => $args, - 'closure_expansion' => false, - ) ); - } -} diff --git a/includes/utils/RowUpdateGenerator.php b/includes/utils/RowUpdateGenerator.php new file mode 100644 index 00000000..6a4792cb --- /dev/null +++ b/includes/utils/RowUpdateGenerator.php @@ -0,0 +1,39 @@ + 'new value', + * 'other_col' => 99, + * ); + * + * @param stdClass $row A row from the database + * @return array Map of column names to updated value within the + * database row. When no update is required returns an empty array. + */ + public function update( $row ); +} diff --git a/includes/utils/UIDGenerator.php b/includes/utils/UIDGenerator.php index 92415877..04c8e194 100644 --- a/includes/utils/UIDGenerator.php +++ b/includes/utils/UIDGenerator.php @@ -20,6 +20,7 @@ * @file * @author Aaron Schulz */ +use Wikimedia\Assert\Assert; /** * Class for getting statistically unique IDs @@ -51,7 +52,7 @@ class UIDGenerator { } // Try to get some ID that uniquely identifies this machine (RFC 4122)... if ( !preg_match( '/^[0-9a-f]{12}$/i', $nodeId ) ) { - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); if ( wfIsWindows() ) { // http://technet.microsoft.com/en-us/library/bb490913.aspx $csv = trim( wfShellExec( 'getmac /NH /FO CSV' ) ); @@ -65,7 +66,7 @@ class UIDGenerator { wfShellExec( '/sbin/ifconfig -a' ), $m ); $nodeId = isset( $m[1] ) ? str_replace( ':', '', $m[1] ) : ''; } - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( !preg_match( '/^[0-9a-f]{12}$/i', $nodeId ) ) { $nodeId = MWCryptRand::generateHex( 12, true ); $nodeId[1] = dechex( hexdec( $nodeId[1] ) | 0x1 ); // set multicast bit @@ -107,9 +108,10 @@ class UIDGenerator { * @throws MWException */ public static function newTimestampedUID88( $base = 10 ) { - if ( !is_integer( $base ) || $base > 36 || $base < 2 ) { - throw new MWException( "Base must an integer be between 2 and 36" ); - } + Assert::parameterType( 'integer', $base, '$base' ); + Assert::parameter( $base <= 36, '$base', 'must be <= 36' ); + Assert::parameter( $base >= 2, '$base', 'must be >= 2' ); + $gen = self::singleton(); $time = $gen->getTimestampAndDelay( 'lockFile88', 1, 1024 ); @@ -152,9 +154,10 @@ class UIDGenerator { * @throws MWException */ public static function newTimestampedUID128( $base = 10 ) { - if ( !is_integer( $base ) || $base > 36 || $base < 2 ) { - throw new MWException( "Base must be an integer between 2 and 36" ); - } + Assert::parameterType( 'integer', $base, '$base' ); + Assert::parameter( $base <= 36, '$base', 'must be <= 36' ); + Assert::parameter( $base >= 2, '$base', 'must be >= 2' ); + $gen = self::singleton(); $time = $gen->getTimestampAndDelay( 'lockFile128', 16384, 1048576 ); @@ -280,7 +283,7 @@ class UIDGenerator { $cache = null; if ( ( $flags & self::QUICK_VOLATILE ) && PHP_SAPI !== 'cli' ) { try { - $cache = ObjectCache::newAccelerator( array() ); + $cache = ObjectCache::newAccelerator(); } catch ( Exception $e ) { // not supported } diff --git a/includes/utils/iterators/IteratorDecorator.php b/includes/utils/iterators/IteratorDecorator.php new file mode 100644 index 00000000..c1b50207 --- /dev/null +++ b/includes/utils/iterators/IteratorDecorator.php @@ -0,0 +1,50 @@ +iterator = $iterator; + } + + public function current() { + return $this->iterator->current(); + } + + public function key() { + return $this->iterator->key(); + } + + public function next() { + $this->iterator->next(); + } + + public function rewind() { + $this->iterator->rewind(); + } + + public function valid() { + return $this->iterator->valid(); + } +} diff --git a/includes/utils/iterators/NotRecursiveIterator.php b/includes/utils/iterators/NotRecursiveIterator.php new file mode 100644 index 00000000..52ca61b4 --- /dev/null +++ b/includes/utils/iterators/NotRecursiveIterator.php @@ -0,0 +1,35 @@ + Date: Fri, 18 Dec 2015 06:04:58 +0100 Subject: Update to MediaWiki 1.26.1 --- RELEASE-NOTES-1.26 | 25 ++++++++++ .../SyntaxHighlight_GeSHi.class.php | 29 ++++++++---- extensions/SyntaxHighlight_GeSHi/composer.json | 2 +- .../maintenance/updateCSS.php | 22 +++++++-- .../maintenance/updateLexerList.php | 23 +++++++-- includes/DefaultSettings.php | 8 +++- includes/Hooks.php | 42 +---------------- includes/HttpFunctions.php | 17 ++++++- includes/MediaWiki.php | 44 +++++++++++++++--- includes/Setup.php | 15 ++++++ includes/User.php | 7 ++- includes/debug/logger/LoggerFactory.php | 2 +- includes/libs/MultiHttpClient.php | 13 ++++++ includes/libs/objectcache/APCBagOStuff.php | 54 +++++++++++++++++++++- includes/specialpage/RedirectSpecialPage.php | 12 +++++ includes/specials/SpecialExpandTemplates.php | 2 +- includes/specials/SpecialMyLanguage.php | 11 +++++ includes/specials/SpecialMyRedirectPages.php | 50 ++++++++++++++++++++ includes/specials/SpecialSearch.php | 2 +- includes/utils/IP.php | 22 ++++++--- .../includes/parser/MediaWikiParserTest.php | 1 + tests/phpunit/includes/parser/NewParserTest.php | 1 + tests/phpunit/includes/utils/IPTest.php | 33 +++++++++++-- tests/phpunit/suite.xml | 4 ++ 24 files changed, 356 insertions(+), 85 deletions(-) (limited to 'includes/utils') diff --git a/RELEASE-NOTES-1.26 b/RELEASE-NOTES-1.26 index 81405f50..0adfbe20 100644 --- a/RELEASE-NOTES-1.26 +++ b/RELEASE-NOTES-1.26 @@ -1,6 +1,31 @@ Security reminder: If you have PHP's register_globals option set, you must turn it off. MediaWiki will not work with it enabled. +== MediaWiki 1.26.1 == + +This is a maintenance release of the MediaWiki 1.26 branch. + +=== Changes since 1.26.0 === +* (T117899) SECURITY: $wgArticlePath can no longer be set to relative paths + that do not begin with a slash. This enabled trivial XSS attacks. + Configuration values such as "http://my.wiki.com/wiki/$1" are fine, as are + "/wiki/$1". A value such as "$1" or "wiki/$1" is not and will now throw an + error. +* (T119309) SECURITY: Use hash_equals() for edit token comparison +* (T118032) SECURITY: Don't allow cURL to interpret POST parameters starting + with '@' as file uploads +* (T115522) SECURITY: Passwords generated by User::randomPassword() can no + longer be shorter than $wgMinimalPasswordLength +* (T97897) SECURITY: Improve IP parsing and trimming. Previous behavior could + result in improper blocks being issued +* (T109724) SECURITY: Special:MyPage, Special:MyTalk, Special:MyContributions + and related pages no longer use HTTP redirects and are now redirected by + MediaWiki +* Fixed ConfigException in ExpandTemplates due to AlwaysUseTidy. +* Fixed stray literal \n in Special:Search. +* Fix issue that breaks HHVM Repo Authorative mode. +* (T120267) Work around APCu memory corruption bug + == MediaWiki 1.26 == === Configuration changes in 1.26 === diff --git a/extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.class.php b/extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.class.php index 535e37af..9eed2763 100644 --- a/extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.class.php +++ b/extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.class.php @@ -16,7 +16,7 @@ * http://www.gnu.org/copyleft/gpl.html */ -use KzykHys\Pygments\Pygments; +use Symfony\Component\Process\ProcessBuilder; // @codingStandardsIgnoreStart class SyntaxHighlight_GeSHi { @@ -276,18 +276,29 @@ class SyntaxHighlight_GeSHi { $output = $cache->get( $cacheKey ); if ( $output === false ) { - try { - $pygments = new Pygments( $wgPygmentizePath ); - $output = $pygments->highlight( $code, $lexer, 'html', $options ); - } catch ( RuntimeException $e ) { + $optionPairs = array(); + foreach ( $options as $k => $v ) { + $optionPairs[] = "{$k}={$v}"; + } + $builder = new ProcessBuilder(); + $builder->setPrefix( $wgPygmentizePath ); + $process = $builder + ->add( '-l' )->add( $lexer ) + ->add( '-f' )->add( 'html' ) + ->add( '-O' )->add( implode( ',', $optionPairs ) ) + ->getProcess(); + + $process->setInput( $code ); + $process->run(); + + if ( !$process->isSuccessful() ) { $status->warning( 'syntaxhighlight-error-pygments-invocation-failure' ); - wfWarn( - 'Failed to invoke Pygments. Please check that Pygments is installed ' . - 'and that $wgPygmentizePath is accurate.' - ); + wfWarn( 'Failed to invoke Pygments: ' . $process->getErrorOutput() ); $status->value = self::highlight( $code, null, $args )->getValue(); return $status; } + + $output = $process->getOutput(); $cache->set( $cacheKey, $output ); } diff --git a/extensions/SyntaxHighlight_GeSHi/composer.json b/extensions/SyntaxHighlight_GeSHi/composer.json index 709c1fb0..d8b8cc8e 100644 --- a/extensions/SyntaxHighlight_GeSHi/composer.json +++ b/extensions/SyntaxHighlight_GeSHi/composer.json @@ -2,7 +2,7 @@ "name": "mediawiki/syntax-highlight_geshi", "description": "Syntax highlighting extension for MediaWiki", "require": { - "kzykhys/pygments": "1.0" + "symfony/process": "~2.5" }, "require-dev": { "jakub-onderka/php-parallel-lint": "0.9", diff --git a/extensions/SyntaxHighlight_GeSHi/maintenance/updateCSS.php b/extensions/SyntaxHighlight_GeSHi/maintenance/updateCSS.php index a3c0c817..9299cd74 100644 --- a/extensions/SyntaxHighlight_GeSHi/maintenance/updateCSS.php +++ b/extensions/SyntaxHighlight_GeSHi/maintenance/updateCSS.php @@ -22,7 +22,7 @@ * @ingroup Maintenance */ -use KzykHys\Pygments\Pygments; +use Symfony\Component\Process\ProcessBuilder; $IP = getenv( 'MW_INSTALL_PATH' ) ?: __DIR__ . '/../../..'; @@ -39,9 +39,25 @@ class UpdateCSS extends Maintenance { global $wgPygmentizePath; $target = __DIR__ . '/../modules/pygments.generated.css'; - $pygments = new Pygments( $wgPygmentizePath ); $css = "/* Stylesheet generated by updateCSS.php */\n"; - $css .= $pygments->getCss( 'default', '.' . SyntaxHighlight_GeSHi::HIGHLIGHT_CSS_CLASS ); + + $builder = new ProcessBuilder(); + $builder->setPrefix( $wgPygmentizePath ); + + $process = $builder + ->add( '-f' )->add( 'html' ) + ->add( '-S' )->add( 'default' ) + ->add( '-a' )->add( '.' . SyntaxHighlight_GeSHi::HIGHLIGHT_CSS_CLASS ) + ->getProcess(); + + $process->run(); + + if ( !$process->isSuccessful() ) { + throw new \RuntimeException( $process->getErrorOutput() ); + } + + $css .= $process->getOutput(); + if ( file_put_contents( $target, $css ) === false ) { $this->output( "Failed to write to {$target}\n" ); } else { diff --git a/extensions/SyntaxHighlight_GeSHi/maintenance/updateLexerList.php b/extensions/SyntaxHighlight_GeSHi/maintenance/updateLexerList.php index 75beb9b5..b5a7fc5a 100644 --- a/extensions/SyntaxHighlight_GeSHi/maintenance/updateLexerList.php +++ b/extensions/SyntaxHighlight_GeSHi/maintenance/updateLexerList.php @@ -22,7 +22,7 @@ * @ingroup Maintenance */ -use KzykHys\Pygments\Pygments; +use Symfony\Component\Process\ProcessBuilder; $IP = getenv( 'MW_INSTALL_PATH' ) ?: __DIR__ . '/../../..'; @@ -43,8 +43,25 @@ class UpdateLanguageList extends Maintenance { $header = '// Generated by ' . basename( __FILE__ ) . "\n\n"; - $pygments = new Pygments( $wgPygmentizePath ); - $lexers = array_keys( $pygments->getLexers() ); + $lexers = array(); + + $builder = new ProcessBuilder(); + $builder->setPrefix( $wgPygmentizePath ); + + $process = $builder->add( '-L' )->add( 'lexer' )->getProcess(); + $process->run(); + + if ( !$process->isSuccessful() ) { + throw new \RuntimeException( $process->getErrorOutput() ); + } + + $output = $process->getOutput(); + foreach ( explode( "\n", $output ) as $line ) { + if ( substr( $line, 0, 1 ) === '*' ) { + $newLexers = explode( ', ', trim( $line, "* :\n" ) ); + $lexers = array_merge( $lexers, $newLexers ); + } + } sort( $lexers ); $code = "getMessage(); - } catch ( Exception $e ) { - restore_error_handler(); - throw $e; - } - - restore_error_handler(); + $retval = call_user_func_array( $callback, $hook_args ); // Process the return value. if ( is_string( $retval ) ) { // String returned means error. throw new FatalError( $retval ); - } elseif ( $badhookmsg !== null ) { - // Exception was thrown from Hooks::hookErrorHandler. - throw new MWException( - 'Detected bug in an extension! ' . - "Hook $func has invalid call signature; " . $badhookmsg - ); } elseif ( $retval === false ) { // False was returned. Stop processing, but no error. return false; @@ -229,27 +212,4 @@ class Hooks { return true; } - - /** - * Handle PHP errors issued inside a hook. Catch errors that have to do - * with a function expecting a reference, and pass all others through to - * MWExceptionHandler::handleError() for default processing. - * - * @since 1.18 - * - * @param int $errno Error number (unused) - * @param string $errstr Error message - * @throws MWHookException If the error has to do with the function signature - * @return bool - */ - public static function hookErrorHandler( $errno, $errstr ) { - if ( strpos( $errstr, 'expected to be a reference, value given' ) !== false ) { - throw new MWHookException( $errstr, $errno ); - } - - // Delegate unhandled errors to the default MW handler - return call_user_func_array( - 'MWExceptionHandler::handleError', func_get_args() - ); - } } diff --git a/includes/HttpFunctions.php b/includes/HttpFunctions.php index bc5a9570..3dff9711 100644 --- a/includes/HttpFunctions.php +++ b/includes/HttpFunctions.php @@ -779,7 +779,22 @@ class CurlHttpRequest extends MWHttpRequest { $this->curlOptions[CURLOPT_HEADER] = true; } elseif ( $this->method == 'POST' ) { $this->curlOptions[CURLOPT_POST] = true; - $this->curlOptions[CURLOPT_POSTFIELDS] = $this->postData; + $postData = $this->postData; + // Don't interpret POST parameters starting with '@' as file uploads, because this + // makes it impossible to POST plain values starting with '@' (and causes security + // issues potentially exposing the contents of local files). + // The PHP manual says this option was introduced in PHP 5.5 defaults to true in PHP 5.6, + // but we support lower versions, and the option doesn't exist in HHVM 5.6.99. + if ( defined( 'CURLOPT_SAFE_UPLOAD' ) ) { + $this->curlOptions[CURLOPT_SAFE_UPLOAD] = true; + } else if ( is_array( $postData ) ) { + // In PHP 5.2 and later, '@' is interpreted as a file upload if POSTFIELDS + // is an array, but not if it's a string. So convert $req['body'] to a string + // for safety. + $postData = wfArrayToCgi( $postData ); + } + $this->curlOptions[CURLOPT_POSTFIELDS] = $postData; + // Suppress 'Expect: 100-continue' header, as some servers // will reject it with a 417 and Curl won't auto retry // with HTTP 1.0 fallback diff --git a/includes/MediaWiki.php b/includes/MediaWiki.php index fbacb250..2da2f6ce 100644 --- a/includes/MediaWiki.php +++ b/includes/MediaWiki.php @@ -36,6 +36,11 @@ class MediaWiki { */ private $config; + /** + * @var String Cache what action this request is + */ + private $action; + /** * @param IContextSource|null $context */ @@ -141,13 +146,11 @@ class MediaWiki { * @return string Action */ public function getAction() { - static $action = null; - - if ( $action === null ) { - $action = Action::getActionName( $this->context ); + if ( $this->action === null ) { + $this->action = Action::getActionName( $this->context ); } - return $action; + return $this->action; } /** @@ -242,8 +245,37 @@ class MediaWiki { // Handle any other redirects. // Redirect loops, titleless URL, $wgUsePathInfo URLs, and URLs with a variant } elseif ( !$this->tryNormaliseRedirect( $title ) ) { + // Prevent information leak via Special:MyPage et al (T109724) + if ( $title->isSpecialPage() ) { + $specialPage = SpecialPageFactory::getPage( $title->getDBKey() ); + if ( $specialPage instanceof RedirectSpecialPage + && $this->config->get( 'HideIdentifiableRedirects' ) + && $specialPage->personallyIdentifiableTarget() + ) { + list( , $subpage ) = SpecialPageFactory::resolveAlias( $title->getDBKey() ); + $target = $specialPage->getRedirect( $subpage ); + // target can also be true. We let that case fall through to normal processing. + if ( $target instanceof Title ) { + $query = $specialPage->getRedirectQuery() ?: array(); + $request = new DerivativeRequest( $this->context->getRequest(), $query ); + $request->setRequestURL( $this->context->getRequest()->getRequestURL() ); + $this->context->setRequest( $request ); + // Do not varnish cache these. May vary even for anons + $this->context->getOutput()->lowerCdnMaxage( 0 ); + $this->context->setTitle( $target ); + $wgTitle = $target; + // Reset action type cache. (Special pages have only view) + $this->action = null; + $title = $target; + $output->addJsConfigVars( array( + 'wgInternalRedirectTargetUrl' => $target->getFullURL( $query ), + ) ); + $output->addModules( 'mediawiki.action.view.redirect' ); + } + } + } - // Special pages + // Special pages ($title may have changed since if statement above) if ( NS_SPECIAL == $title->getNamespace() ) { // Actions that need to be made when we have a special pages SpecialPageFactory::executePath( $title, $this->context ); diff --git a/includes/Setup.php b/includes/Setup.php index 70e8cde4..905a1d10 100644 --- a/includes/Setup.php +++ b/includes/Setup.php @@ -510,6 +510,21 @@ MWExceptionHandler::installHandler(); require_once "$IP/includes/compat/normal/UtfNormalUtil.php"; + +$ps_validation = Profiler::instance()->scopedProfileIn( $fname . '-validation' ); + +// T48998: Bail out early if $wgArticlePath is non-absolute +if ( !preg_match( '/^(https?:\/\/|\/)/', $wgArticlePath ) ) { + throw new FatalError( + 'If you use a relative URL for $wgArticlePath, it must start ' . + 'with a slash (/).

See ' . + '' . + 'https://www.mediawiki.org/wiki/Manual:$wgArticlePath.' + ); +} + +Profiler::instance()->scopedProfileOut( $ps_validation ); + $ps_default2 = Profiler::instance()->scopedProfileIn( $fname . '-defaults2' ); if ( $wgScriptExtension !== '.php' || defined( 'MW_ENTRY_PHP5' ) ) { diff --git a/includes/User.php b/includes/User.php index 22c90cdd..199dd1dc 100644 --- a/includes/User.php +++ b/includes/User.php @@ -1029,11 +1029,10 @@ class User implements IDBAccessObject { // stopping at a minimum of 10 chars. $length = max( 10, $wgMinimalPasswordLength ); // Multiply by 1.25 to get the number of hex characters we need - $length = $length * 1.25; // Generate random hex chars - $hex = MWCryptRand::generateHex( $length ); + $hex = MWCryptRand::generateHex( ceil( $length * 1.25 ) ); // Convert from base 16 to base 32 to get a proper password like string - return wfBaseConvert( $hex, 16, 32 ); + return substr( wfBaseConvert( $hex, 16, 32, $length ), -$length ); } /** @@ -4177,7 +4176,7 @@ class User implements IDBAccessObject { $salt, $request ?: $this->getRequest(), $timestamp ); - if ( $val != $sessionToken ) { + if ( !hash_equals( $sessionToken, $val ) ) { wfDebug( "User::matchEditToken: broken session data\n" ); } diff --git a/includes/debug/logger/LoggerFactory.php b/includes/debug/logger/LoggerFactory.php index 0b6965ff..1e44b708 100644 --- a/includes/debug/logger/LoggerFactory.php +++ b/includes/debug/logger/LoggerFactory.php @@ -94,7 +94,7 @@ class LoggerFactory { * @return \\Psr\\Log\\LoggerInterface */ public static function getInstance( $channel ) { - if ( !interface_exists( '\Psr\Log\LoggerInterface' ) ) { + if ( !interface_exists( 'Psr\Log\LoggerInterface' ) ) { $message = ( 'MediaWiki requires the PSR-3 logging ' . "library to be present. This library is not embedded directly in MediaWiki's " . diff --git a/includes/libs/MultiHttpClient.php b/includes/libs/MultiHttpClient.php index 6af3ed51..5555cbcb 100644 --- a/includes/libs/MultiHttpClient.php +++ b/includes/libs/MultiHttpClient.php @@ -335,6 +335,19 @@ class MultiHttpClient { ); } elseif ( $req['method'] === 'POST' ) { curl_setopt( $ch, CURLOPT_POST, 1 ); + // Don't interpret POST parameters starting with '@' as file uploads, because this + // makes it impossible to POST plain values starting with '@' (and causes security + // issues potentially exposing the contents of local files). + // The PHP manual says this option was introduced in PHP 5.5 defaults to true in PHP 5.6, + // but we support lower versions, and the option doesn't exist in HHVM 5.6.99. + if ( defined( 'CURLOPT_SAFE_UPLOAD' ) ) { + curl_setopt( $ch, CURLOPT_SAFE_UPLOAD, true ); + } else if ( is_array( $req['body'] ) ) { + // In PHP 5.2 and later, '@' is interpreted as a file upload if POSTFIELDS + // is an array, but not if it's a string. So convert $req['body'] to a string + // for safety. + $req['body'] = wfArrayToCgi( $req['body'] ); + } curl_setopt( $ch, CURLOPT_POSTFIELDS, $req['body'] ); } else { if ( is_resource( $req['body'] ) || $req['body'] !== '' ) { diff --git a/includes/libs/objectcache/APCBagOStuff.php b/includes/libs/objectcache/APCBagOStuff.php index 0dbbaba9..35e05e80 100644 --- a/includes/libs/objectcache/APCBagOStuff.php +++ b/includes/libs/objectcache/APCBagOStuff.php @@ -27,22 +27,72 @@ * @ingroup Cache */ class APCBagOStuff extends BagOStuff { + + /** + * @var bool If true, trust the APC implementation to serialize and + * deserialize objects correctly. If false, (de-)serialize in PHP. + */ + protected $nativeSerialize; + /** * @var string String to append to each APC key. This may be changed * whenever the handling of values is changed, to prevent existing code * from encountering older values which it cannot handle. - **/ - const KEY_SUFFIX = ':1'; + */ + const KEY_SUFFIX = ':2'; + + /** + * Constructor + * + * Available parameters are: + * - nativeSerialize: If true, pass objects to apc_store(), and trust it + * to serialize them correctly. If false, serialize + * all values in PHP. + * + * @param array $params + */ + public function __construct( array $params = array() ) { + parent::__construct( $params ); + + if ( isset( $params['nativeSerialize'] ) ) { + $this->nativeSerialize = $params['nativeSerialize']; + } elseif ( extension_loaded( 'apcu' ) && ini_get( 'apc.serializer' ) === 'default' ) { + // APCu has a memory corruption bug when the serializer is set to 'default'. + // See T120267, and upstream bug reports: + // - https://github.com/krakjoe/apcu/issues/38 + // - https://github.com/krakjoe/apcu/issues/35 + // - https://github.com/krakjoe/apcu/issues/111 + $this->logger->warning( + 'The APCu extension is loaded and the apc.serializer INI setting ' . + 'is set to "default". This can cause memory corruption! ' . + 'You should change apc.serializer to "php" instead. ' . + 'See .' + ); + $this->nativeSerialize = false; + } else { + $this->nativeSerialize = true; + } + } public function get( $key, &$casToken = null, $flags = 0 ) { $val = apc_fetch( $key . self::KEY_SUFFIX ); $casToken = $val; + if ( is_string( $val ) && !$this->nativeSerialize ) { + $val = $this->isInteger( $val ) + ? intval( $val ) + : unserialize( $val ); + } + return $val; } public function set( $key, $value, $exptime = 0 ) { + if ( !$this->nativeSerialize && !$this->isInteger( $value ) ) { + $value = serialize( $value ); + } + apc_store( $key . self::KEY_SUFFIX, $value, $exptime ); return true; diff --git a/includes/specialpage/RedirectSpecialPage.php b/includes/specialpage/RedirectSpecialPage.php index 9129ee5d..5047354e 100644 --- a/includes/specialpage/RedirectSpecialPage.php +++ b/includes/specialpage/RedirectSpecialPage.php @@ -94,6 +94,18 @@ abstract class RedirectSpecialPage extends UnlistedSpecialPage { ? $params : false; } + + /** + * Indicate if the target of this redirect can be used to identify + * a particular user of this wiki (e.g., if the redirect is to the + * user page of a User). See T109724. + * + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return false; + } } /** diff --git a/includes/specials/SpecialExpandTemplates.php b/includes/specials/SpecialExpandTemplates.php index b7582e6c..06eb2769 100644 --- a/includes/specials/SpecialExpandTemplates.php +++ b/includes/specials/SpecialExpandTemplates.php @@ -114,7 +114,7 @@ class SpecialExpandTemplates extends SpecialPage { } $config = $this->getConfig(); - if ( ( $config->get( 'UseTidy' ) && $options->getTidy() ) || $config->get( 'AlwaysUseTidy' ) ) { + if ( $config->get( 'UseTidy' ) && $options->getTidy() ) { $tmp = MWTidy::tidy( $tmp ); } diff --git a/includes/specials/SpecialMyLanguage.php b/includes/specials/SpecialMyLanguage.php index 3d8ff97b..d11fbe63 100644 --- a/includes/specials/SpecialMyLanguage.php +++ b/includes/specials/SpecialMyLanguage.php @@ -99,4 +99,15 @@ class SpecialMyLanguage extends RedirectSpecialArticle { return $base; } } + + /** + * Target can identify a specific user's language preference. + * + * @see T109724 + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return true; + } } diff --git a/includes/specials/SpecialMyRedirectPages.php b/includes/specials/SpecialMyRedirectPages.php index 5ef03f13..850b1f63 100644 --- a/includes/specials/SpecialMyRedirectPages.php +++ b/includes/specials/SpecialMyRedirectPages.php @@ -45,6 +45,16 @@ class SpecialMypage extends RedirectSpecialArticle { return Title::makeTitle( NS_USER, $this->getUser()->getName() . '/' . $subpage ); } + + /** + * Target identifies a specific User. See T109724. + * + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return true; + } } /** @@ -68,6 +78,16 @@ class SpecialMytalk extends RedirectSpecialArticle { return Title::makeTitle( NS_USER_TALK, $this->getUser()->getName() . '/' . $subpage ); } + + /** + * Target identifies a specific User. See T109724. + * + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return true; + } } /** @@ -90,6 +110,16 @@ class SpecialMycontributions extends RedirectSpecialPage { public function getRedirect( $subpage ) { return SpecialPage::getTitleFor( 'Contributions', $this->getUser()->getName() ); } + + /** + * Target identifies a specific User. See T109724. + * + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return true; + } } /** @@ -110,6 +140,16 @@ class SpecialMyuploads extends RedirectSpecialPage { public function getRedirect( $subpage ) { return SpecialPage::getTitleFor( 'Listfiles', $this->getUser()->getName() ); } + + /** + * Target identifies a specific User. See T109724. + * + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return true; + } } /** @@ -132,4 +172,14 @@ class SpecialAllMyUploads extends RedirectSpecialPage { return SpecialPage::getTitleFor( 'Listfiles', $this->getUser()->getName() ); } + + /** + * Target identifies a specific User. See T109724. + * + * @since 1.27 + * @return bool + */ + public function personallyIdentifiableTarget() { + return true; + } } diff --git a/includes/specials/SpecialSearch.php b/includes/specials/SpecialSearch.php index f50fb732..8c546edd 100644 --- a/includes/specials/SpecialSearch.php +++ b/includes/specials/SpecialSearch.php @@ -388,7 +388,7 @@ class SpecialSearch extends SpecialPage { } } - $out->addHTML( '
\n' ); + $out->addHTML( '
' ); if ( $prevnext ) { $out->addHTML( "

{$prevnext}

\n" ); } diff --git a/includes/utils/IP.php b/includes/utils/IP.php index 666660aa..13586f3c 100644 --- a/includes/utils/IP.php +++ b/includes/utils/IP.php @@ -132,8 +132,9 @@ class IP { /** * Convert an IP into a verbose, uppercase, normalized form. - * IPv6 addresses in octet notation are expanded to 8 words. - * IPv4 addresses are just trimmed. + * Both IPv4 and IPv6 addresses are trimmed. Additionally, + * IPv6 addresses in octet notation are expanded to 8 words; + * IPv4 addresses have leading zeros, in each octet, removed. * * @param string $ip IP address in quad or octet form (CIDR or not). * @return string @@ -143,8 +144,16 @@ class IP { if ( $ip === '' ) { return null; } - if ( self::isIPv4( $ip ) || !self::isIPv6( $ip ) ) { - return $ip; // nothing else to do for IPv4 addresses or invalid ones + /* If not an IP, just return trimmed value, since sanitizeIP() is called + * in a number of contexts where usernames are supplied as input. + */ + if ( !self::isIPAddress($ip) ) { + return $ip; + } + if ( self::isIPv4( $ip ) ) { + // Remove leading 0's from octet representation of IPv4 address + $ip = preg_replace( '/(?:^|(?<=\.))0+(?=[1-9]|0\.|0$)/', '', $ip ); + return $ip; } // Remove any whitespaces, convert to upper case $ip = strtoupper( $ip ); @@ -399,8 +408,9 @@ class IP { if ( self::isIPv6( $ip ) ) { $n = 'v6-' . self::IPv6ToRawHex( $ip ); } elseif ( self::isIPv4( $ip ) ) { - // Bug 60035: an IP with leading 0's fails in ip2long sometimes (e.g. *.08) - $ip = preg_replace( '/(?<=\.)0+(?=[1-9])/', '', $ip ); + // T62035/T97897: An IP with leading 0's fails in ip2long sometimes (e.g. *.08), + // also double/triple 0 needs to be changed to just a single 0 for ip2long. + $ip = self::sanitizeIP( $ip ); $n = ip2long( $ip ); if ( $n < 0 ) { $n += pow( 2, 32 ); diff --git a/tests/phpunit/includes/parser/MediaWikiParserTest.php b/tests/phpunit/includes/parser/MediaWikiParserTest.php index 96ae3bec..210c17c5 100644 --- a/tests/phpunit/includes/parser/MediaWikiParserTest.php +++ b/tests/phpunit/includes/parser/MediaWikiParserTest.php @@ -7,6 +7,7 @@ require_once __DIR__ . '/NewParserTest.php'; * an PHPUnit_Framework_Test object * * @group Parser + * @group ParserTests * @group Database */ class MediaWikiParserTest { diff --git a/tests/phpunit/includes/parser/NewParserTest.php b/tests/phpunit/includes/parser/NewParserTest.php index df7da98c..d95e9225 100644 --- a/tests/phpunit/includes/parser/NewParserTest.php +++ b/tests/phpunit/includes/parser/NewParserTest.php @@ -672,6 +672,7 @@ class NewParserTest extends MediaWikiTestCase { /** * @group medium + * @group ParserTests * @dataProvider parserTestProvider * @param string $desc * @param string $input diff --git a/tests/phpunit/includes/utils/IPTest.php b/tests/phpunit/includes/utils/IPTest.php index 04b8f486..34aff796 100644 --- a/tests/phpunit/includes/utils/IPTest.php +++ b/tests/phpunit/includes/utils/IPTest.php @@ -307,12 +307,34 @@ class IPTest extends PHPUnit_Framework_TestCase { } /** - * Improve IP::sanitizeIP() code coverage - * @todo Most probably incomplete + * @covers IP::sanitizeIP + * @dataProvider provideSanitizeIP */ - public function testSanitizeIP() { - $this->assertNull( IP::sanitizeIP( '' ) ); - $this->assertNull( IP::sanitizeIP( ' ' ) ); + public function testSanitizeIP( $expected, $input ) { + $result = IP::sanitizeIP( $input ); + $this->assertEquals( $expected, $result ); + } + + /** + * Provider for IP::testSanitizeIP() + */ + public static function provideSanitizeIP() { + return array( + array( '0.0.0.0', '0.0.0.0' ), + array( '0.0.0.0', '00.00.00.00' ), + array( '0.0.0.0', '000.000.000.000' ), + array( '141.0.11.253', '141.000.011.253' ), + array( '1.2.4.5', '1.2.4.5' ), + array( '1.2.4.5', '01.02.04.05' ), + array( '1.2.4.5', '001.002.004.005' ), + array( '10.0.0.1', '010.0.000.1' ), + array( '80.72.250.4', '080.072.250.04' ), + array( 'Foo.1000.00', 'Foo.1000.00'), + array( 'Bar.01', 'Bar.01'), + array( 'Bar.010', 'Bar.010'), + array( null, ''), + array( null, ' ') + ); } /** @@ -336,6 +358,7 @@ class IPTest extends PHPUnit_Framework_TestCase { array( '80000000', '128.0.0.0' ), array( 'DEADCAFE', '222.173.202.254' ), array( 'FFFFFFFF', '255.255.255.255' ), + array( '8D000BFD', '141.000.11.253' ), array( false, 'IN.VA.LI.D' ), array( 'v6-00000000000000000000000000000001', '::1' ), array( 'v6-20010DB885A3000000008A2E03707334', '2001:0db8:85a3:0000:0000:8a2e:0370:7334' ), diff --git a/tests/phpunit/suite.xml b/tests/phpunit/suite.xml index 1acbc241..82086b9d 100644 --- a/tests/phpunit/suite.xml +++ b/tests/phpunit/suite.xml @@ -24,6 +24,10 @@ phpunit.php enables colors for other OSs at runtime languages + + includes/parser/MediaWikiParserTest.php + suites/ExtensionsParserTestSuite.php + skins -- cgit v1.2.3-54-g00ecf