Merge "HISTORY: Add MediaWiki 1.3 post-release change notes"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 28 Mar 2019 18:38:12 +0000 (18:38 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 28 Mar 2019 18:38:12 +0000 (18:38 +0000)
42 files changed:
.phpcs.xml
autoload.php
includes/DefaultSettings.php
includes/HistoryBlob.php [deleted file]
includes/Linker.php
includes/MovePage.php
includes/SiteConfiguration.php
includes/Title.php
includes/actions/RollbackAction.php
includes/api/ApiQuerySiteinfo.php
includes/api/ApiStashEdit.php
includes/content/WikiTextStructure.php
includes/historyblob/ConcatenatedGzipHistoryBlob.php [new file with mode: 0644]
includes/historyblob/DiffHistoryBlob.php [new file with mode: 0644]
includes/historyblob/HistoryBlob.php [new file with mode: 0644]
includes/historyblob/HistoryBlobCurStub.php [new file with mode: 0644]
includes/historyblob/HistoryBlobStub.php [new file with mode: 0644]
includes/http/PhpHttpRequest.php
includes/jobqueue/jobs/ThumbnailRenderJob.php
includes/libs/objectcache/BagOStuff.php
includes/libs/objectcache/WANObjectCache.php
includes/libs/rdbms/database/DatabaseDomain.php
includes/preferences/DefaultPreferencesFactory.php
includes/specialpage/ChangesListSpecialPage.php
includes/specialpage/FormSpecialPage.php
includes/specialpage/QueryPage.php
includes/specials/SpecialActiveusers.php
includes/specials/SpecialAllMessages.php
includes/specials/SpecialAutoblockList.php
includes/specials/SpecialBlockList.php
includes/specials/SpecialBooksources.php
includes/specials/SpecialComparePages.php
includes/specials/SpecialListusers.php
includes/specials/SpecialNewpages.php
includes/specials/SpecialRecentchanges.php
includes/specials/SpecialSearch.php
includes/specials/SpecialUpload.php
includes/specials/SpecialUploadStash.php
languages/i18n/en.json
languages/i18n/qqq.json
profileinfo.php
resources/Resources.php

index d1e54a7..ce0eac4 100644 (file)
                <exclude-pattern>*/includes/Feed\.php</exclude-pattern>
                <exclude-pattern>*/includes/filerepo/file/LocalFile\.php</exclude-pattern>
                <exclude-pattern>*/includes/gallery/PackedOverlayImageGallery\.php</exclude-pattern>
-               <exclude-pattern>*/includes/HistoryBlob\.php</exclude-pattern>
                <exclude-pattern>*/includes/htmlform/HTMLFormElement\.php</exclude-pattern>
                <exclude-pattern>*/includes/libs/filebackend/FileBackendStore\.php</exclude-pattern>
                <exclude-pattern>*/includes/libs/filebackend/FSFileBackend\.php</exclude-pattern>
index 528b7fe..bb0457b 100644 (file)
@@ -298,7 +298,7 @@ $wgAutoloadLocalClasses = [
        'ComposerVendorHtaccessCreator' => __DIR__ . '/includes/composer/ComposerVendorHtaccessCreator.php',
        'ComposerVersionNormalizer' => __DIR__ . '/includes/composer/ComposerVersionNormalizer.php',
        'CompressOld' => __DIR__ . '/maintenance/storage/compressOld.php',
-       'ConcatenatedGzipHistoryBlob' => __DIR__ . '/includes/HistoryBlob.php',
+       'ConcatenatedGzipHistoryBlob' => __DIR__ . '/includes/historyblob/ConcatenatedGzipHistoryBlob.php',
        'Config' => __DIR__ . '/includes/config/Config.php',
        'ConfigException' => __DIR__ . '/includes/config/ConfigException.php',
        'ConfigFactory' => __DIR__ . '/includes/config/ConfigFactory.php',
@@ -398,7 +398,7 @@ $wgAutoloadLocalClasses = [
        'Diff' => __DIR__ . '/includes/diff/DairikiDiff.php',
        'DiffEngine' => __DIR__ . '/includes/diff/DiffEngine.php',
        'DiffFormatter' => __DIR__ . '/includes/diff/DiffFormatter.php',
-       'DiffHistoryBlob' => __DIR__ . '/includes/HistoryBlob.php',
+       'DiffHistoryBlob' => __DIR__ . '/includes/historyblob/DiffHistoryBlob.php',
        'DiffOp' => __DIR__ . '/includes/diff/DairikiDiff.php',
        'DiffOpAdd' => __DIR__ . '/includes/diff/DairikiDiff.php',
        'DiffOpChange' => __DIR__ . '/includes/diff/DairikiDiff.php',
@@ -628,9 +628,9 @@ $wgAutoloadLocalClasses = [
        'HashSiteStore' => __DIR__ . '/includes/site/HashSiteStore.php',
        'HashtableReplacer' => __DIR__ . '/includes/libs/replacers/HashtableReplacer.php',
        'HistoryAction' => __DIR__ . '/includes/actions/HistoryAction.php',
-       'HistoryBlob' => __DIR__ . '/includes/HistoryBlob.php',
-       'HistoryBlobCurStub' => __DIR__ . '/includes/HistoryBlob.php',
-       'HistoryBlobStub' => __DIR__ . '/includes/HistoryBlob.php',
+       'HistoryBlob' => __DIR__ . '/includes/historyblob/HistoryBlob.php',
+       'HistoryBlobCurStub' => __DIR__ . '/includes/historyblob/HistoryBlobCurStub.php',
+       'HistoryBlobStub' => __DIR__ . '/includes/historyblob/HistoryBlobStub.php',
        'HistoryPager' => __DIR__ . '/includes/actions/pagers/HistoryPager.php',
        'Hooks' => __DIR__ . '/includes/Hooks.php',
        'Html' => __DIR__ . '/includes/Html.php',
@@ -1707,8 +1707,8 @@ $wgAutoloadLocalClasses = [
        'ZhConverter' => __DIR__ . '/languages/classes/LanguageZh.php',
        'ZipDirectoryReader' => __DIR__ . '/includes/utils/ZipDirectoryReader.php',
        'ZipDirectoryReaderError' => __DIR__ . '/includes/utils/ZipDirectoryReaderError.php',
-       'concatenatedgziphistoryblob' => __DIR__ . '/includes/HistoryBlob.php',
-       'historyblobcurstub' => __DIR__ . '/includes/HistoryBlob.php',
-       'historyblobstub' => __DIR__ . '/includes/HistoryBlob.php',
+       'concatenatedgziphistoryblob' => __DIR__ . '/includes/historyblob/ConcatenatedGzipHistoryBlob.php',
+       'historyblobcurstub' => __DIR__ . '/includes/historyblob/HistoryBlobCurStub.php',
+       'historyblobstub' => __DIR__ . '/includes/historyblob/HistoryBlobStub.php',
        'profile_point' => __DIR__ . '/profileinfo.php',
 ];
index d173d35..3a040c8 100644 (file)
@@ -9030,16 +9030,6 @@ $wgPriorityHints = false;
  */
 $wgElementTiming = false;
 
-/**
- * Temporary option to show rollback confirmation user settings
- * without activating the feature itself
- * @see T217039
- * @since 1.33
- * @deprecated 1.33
- * @var bool
- */
-$wgDisableRollbackConfirmationFeature = false;
-
 /**
  * For really cool vim folding this needs to be at the end:
  * vim: foldmarker=@{,@} foldmethod=marker
diff --git a/includes/HistoryBlob.php b/includes/HistoryBlob.php
deleted file mode 100644 (file)
index bca6c7e..0000000
+++ /dev/null
@@ -1,711 +0,0 @@
-<?php
-/**
- * Efficient concatenated text storage.
- *
- * 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
- */
-
-/**
- * Base class for general text storage via the "object" flag in old_flags, or
- * two-part external storage URLs. Used for represent efficient concatenated
- * storage, and migration-related pointer objects.
- */
-interface HistoryBlob {
-       /**
-        * Adds an item of text, returns a stub object which points to the item.
-        * You must call setLocation() on the stub object before storing it to the
-        * database
-        *
-        * @param string $text
-        *
-        * @return string The key for getItem()
-        */
-       function addItem( $text );
-
-       /**
-        * Get item by key, or false if the key is not present
-        *
-        * @param string $key
-        *
-        * @return string|bool
-        */
-       function getItem( $key );
-
-       /**
-        * Set the "default text"
-        * This concept is an odd property of the current DB schema, whereby each text item has a revision
-        * associated with it. The default text is the text of the associated revision. There may, however,
-        * be other revisions in the same object.
-        *
-        * Default text is not required for two-part external storage URLs.
-        *
-        * @param string $text
-        */
-       function setText( $text );
-
-       /**
-        * Get default text. This is called from Revision::getRevisionText()
-        *
-        * @return string
-        */
-       function getText();
-}
-
-/**
- * Concatenated gzip (CGZ) storage
- * Improves compression ratio by concatenating like objects before gzipping
- */
-class ConcatenatedGzipHistoryBlob implements HistoryBlob {
-       public $mVersion = 0, $mCompressed = false, $mItems = [], $mDefaultHash = '';
-       public $mSize = 0;
-       public $mMaxSize = 10000000;
-       public $mMaxCount = 100;
-
-       public function __construct() {
-               if ( !function_exists( 'gzdeflate' ) ) {
-                       throw new MWException( "Need zlib support to read or write this "
-                               . "kind of history object (ConcatenatedGzipHistoryBlob)\n" );
-               }
-       }
-
-       /**
-        * @param string $text
-        * @return string
-        */
-       public function addItem( $text ) {
-               $this->uncompress();
-               $hash = md5( $text );
-               if ( !isset( $this->mItems[$hash] ) ) {
-                       $this->mItems[$hash] = $text;
-                       $this->mSize += strlen( $text );
-               }
-               return $hash;
-       }
-
-       /**
-        * @param string $hash
-        * @return array|bool
-        */
-       public function getItem( $hash ) {
-               $this->uncompress();
-               if ( array_key_exists( $hash, $this->mItems ) ) {
-                       return $this->mItems[$hash];
-               } else {
-                       return false;
-               }
-       }
-
-       /**
-        * @param string $text
-        * @return void
-        */
-       public function setText( $text ) {
-               $this->uncompress();
-               $this->mDefaultHash = $this->addItem( $text );
-       }
-
-       /**
-        * @return array|bool
-        */
-       public function getText() {
-               $this->uncompress();
-               return $this->getItem( $this->mDefaultHash );
-       }
-
-       /**
-        * Remove an item
-        *
-        * @param string $hash
-        */
-       public function removeItem( $hash ) {
-               $this->mSize -= strlen( $this->mItems[$hash] );
-               unset( $this->mItems[$hash] );
-       }
-
-       /**
-        * Compress the bulk data in the object
-        */
-       public function compress() {
-               if ( !$this->mCompressed ) {
-                       $this->mItems = gzdeflate( serialize( $this->mItems ) );
-                       $this->mCompressed = true;
-               }
-       }
-
-       /**
-        * Uncompress bulk data
-        */
-       public function uncompress() {
-               if ( $this->mCompressed ) {
-                       $this->mItems = unserialize( gzinflate( $this->mItems ) );
-                       $this->mCompressed = false;
-               }
-       }
-
-       /**
-        * @return array
-        */
-       function __sleep() {
-               $this->compress();
-               return [ 'mVersion', 'mCompressed', 'mItems', 'mDefaultHash' ];
-       }
-
-       function __wakeup() {
-               $this->uncompress();
-       }
-
-       /**
-        * Helper function for compression jobs
-        * Returns true until the object is "full" and ready to be committed
-        *
-        * @return bool
-        */
-       public function isHappy() {
-               return $this->mSize < $this->mMaxSize
-                       && count( $this->mItems ) < $this->mMaxCount;
-       }
-}
-
-/**
- * Pointer object for an item within a CGZ blob stored in the text table.
- */
-class HistoryBlobStub {
-       /**
-        * @var array One-step cache variable to hold base blobs; operations that
-        * pull multiple revisions may often pull multiple times from the same
-        * blob. By keeping the last-used one open, we avoid redundant
-        * unserialization and decompression overhead.
-        */
-       protected static $blobCache = [];
-
-       /** @var int */
-       public $mOldId;
-
-       /** @var string */
-       public $mHash;
-
-       /** @var string */
-       public $mRef;
-
-       /**
-        * @param string $hash The content hash of the text
-        * @param int $oldid The old_id for the CGZ object
-        */
-       function __construct( $hash = '', $oldid = 0 ) {
-               $this->mHash = $hash;
-       }
-
-       /**
-        * Sets the location (old_id) of the main object to which this object
-        * points
-        * @param int $id
-        */
-       function setLocation( $id ) {
-               $this->mOldId = $id;
-       }
-
-       /**
-        * Sets the location (old_id) of the referring object
-        * @param string $id
-        */
-       function setReferrer( $id ) {
-               $this->mRef = $id;
-       }
-
-       /**
-        * Gets the location of the referring object
-        * @return string
-        */
-       function getReferrer() {
-               return $this->mRef;
-       }
-
-       /**
-        * @return string|false
-        */
-       function getText() {
-               if ( isset( self::$blobCache[$this->mOldId] ) ) {
-                       $obj = self::$blobCache[$this->mOldId];
-               } else {
-                       $dbr = wfGetDB( DB_REPLICA );
-                       $row = $dbr->selectRow(
-                               'text',
-                               [ 'old_flags', 'old_text' ],
-                               [ 'old_id' => $this->mOldId ]
-                       );
-
-                       if ( !$row ) {
-                               return false;
-                       }
-
-                       $flags = explode( ',', $row->old_flags );
-                       if ( in_array( 'external', $flags ) ) {
-                               $url = $row->old_text;
-                               $parts = explode( '://', $url, 2 );
-                               if ( !isset( $parts[1] ) || $parts[1] == '' ) {
-                                       return false;
-                               }
-                               $row->old_text = ExternalStore::fetchFromURL( $url );
-
-                       }
-
-                       if ( !in_array( 'object', $flags ) ) {
-                               return false;
-                       }
-
-                       if ( in_array( 'gzip', $flags ) ) {
-                               // This shouldn't happen, but a bug in the compress script
-                               // may at times gzip-compress a HistoryBlob object row.
-                               $obj = unserialize( gzinflate( $row->old_text ) );
-                       } else {
-                               $obj = unserialize( $row->old_text );
-                       }
-
-                       if ( !is_object( $obj ) ) {
-                               // Correct for old double-serialization bug.
-                               $obj = unserialize( $obj );
-                       }
-
-                       // Save this item for reference; if pulling many
-                       // items in a row we'll likely use it again.
-                       $obj->uncompress();
-                       self::$blobCache = [ $this->mOldId => $obj ];
-               }
-
-               return $obj->getItem( $this->mHash );
-       }
-
-       /**
-        * Get the content hash
-        *
-        * @return string
-        */
-       function getHash() {
-               return $this->mHash;
-       }
-}
-
-/**
- * To speed up conversion from 1.4 to 1.5 schema, text rows can refer to the
- * leftover cur table as the backend. This avoids expensively copying hundreds
- * of megabytes of data during the conversion downtime.
- *
- * Serialized HistoryBlobCurStub objects will be inserted into the text table
- * on conversion if $wgLegacySchemaConversion is set to true.
- */
-class HistoryBlobCurStub {
-       /** @var int */
-       public $mCurId;
-
-       /**
-        * @param int $curid The cur_id pointed to
-        */
-       function __construct( $curid = 0 ) {
-               $this->mCurId = $curid;
-       }
-
-       /**
-        * Sets the location (cur_id) of the main object to which this object
-        * points
-        *
-        * @param int $id
-        */
-       function setLocation( $id ) {
-               $this->mCurId = $id;
-       }
-
-       /**
-        * @return string|bool
-        */
-       function getText() {
-               $dbr = wfGetDB( DB_REPLICA );
-               $row = $dbr->selectRow( 'cur', [ 'cur_text' ], [ 'cur_id' => $this->mCurId ] );
-               if ( !$row ) {
-                       return false;
-               }
-               return $row->cur_text;
-       }
-}
-
-/**
- * Diff-based history compression
- * Requires xdiff 1.5+ and zlib
- */
-class DiffHistoryBlob implements HistoryBlob {
-       /** @var array Uncompressed item cache */
-       public $mItems = [];
-
-       /** @var int Total uncompressed size */
-       public $mSize = 0;
-
-       /**
-        * @var array Array of diffs. If a diff D from A to B is notated D = B - A,
-        * and Z is an empty string:
-        *
-        *              { item[map[i]] - item[map[i-1]]   where i > 0
-        *    diff[i] = {
-        *              { item[map[i]] - Z                where i = 0
-        */
-       public $mDiffs;
-
-       /** @var array The diff map, see above */
-       public $mDiffMap;
-
-       /** @var int The key for getText()
-        */
-       public $mDefaultKey;
-
-       /** @var string Compressed storage */
-       public $mCompressed;
-
-       /** @var bool True if the object is locked against further writes */
-       public $mFrozen = false;
-
-       /**
-        * @var int The maximum uncompressed size before the object becomes sad
-        * Should be less than max_allowed_packet
-        */
-       public $mMaxSize = 10000000;
-
-       /** @var int The maximum number of text items before the object becomes sad */
-       public $mMaxCount = 100;
-
-       /** Constants from xdiff.h */
-       const XDL_BDOP_INS = 1;
-       const XDL_BDOP_CPY = 2;
-       const XDL_BDOP_INSB = 3;
-
-       function __construct() {
-               if ( !function_exists( 'gzdeflate' ) ) {
-                       throw new MWException( "Need zlib support to read or write DiffHistoryBlob\n" );
-               }
-       }
-
-       /**
-        * @throws MWException
-        * @param string $text
-        * @return int
-        */
-       function addItem( $text ) {
-               if ( $this->mFrozen ) {
-                       throw new MWException( __METHOD__ . ": Cannot add more items after sleep/wakeup" );
-               }
-
-               $this->mItems[] = $text;
-               $this->mSize += strlen( $text );
-               $this->mDiffs = null; // later
-               return count( $this->mItems ) - 1;
-       }
-
-       /**
-        * @param string $key
-        * @return string
-        */
-       function getItem( $key ) {
-               return $this->mItems[$key];
-       }
-
-       /**
-        * @param string $text
-        */
-       function setText( $text ) {
-               $this->mDefaultKey = $this->addItem( $text );
-       }
-
-       /**
-        * @return string
-        */
-       function getText() {
-               return $this->getItem( $this->mDefaultKey );
-       }
-
-       /**
-        * @throws MWException
-        */
-       function compress() {
-               if ( !function_exists( 'xdiff_string_rabdiff' ) ) {
-                       throw new MWException( "Need xdiff 1.5+ support to write DiffHistoryBlob\n" );
-               }
-               if ( isset( $this->mDiffs ) ) {
-                       // Already compressed
-                       return;
-               }
-               if ( $this->mItems === [] ) {
-                       return;
-               }
-
-               // Create two diff sequences: one for main text and one for small text
-               $sequences = [
-                       'small' => [
-                               'tail' => '',
-                               'diffs' => [],
-                               'map' => [],
-                       ],
-                       'main' => [
-                               'tail' => '',
-                               'diffs' => [],
-                               'map' => [],
-                       ],
-               ];
-               $smallFactor = 0.5;
-
-               $mItemsCount = count( $this->mItems );
-               for ( $i = 0; $i < $mItemsCount; $i++ ) {
-                       $text = $this->mItems[$i];
-                       if ( $i == 0 ) {
-                               $seqName = 'main';
-                       } else {
-                               $mainTail = $sequences['main']['tail'];
-                               if ( strlen( $text ) < strlen( $mainTail ) * $smallFactor ) {
-                                       $seqName = 'small';
-                               } else {
-                                       $seqName = 'main';
-                               }
-                       }
-                       $seq =& $sequences[$seqName];
-                       $tail = $seq['tail'];
-                       $diff = $this->diff( $tail, $text );
-                       $seq['diffs'][] = $diff;
-                       $seq['map'][] = $i;
-                       $seq['tail'] = $text;
-               }
-               unset( $seq ); // unlink dangerous alias
-
-               // Knit the sequences together
-               $tail = '';
-               $this->mDiffs = [];
-               $this->mDiffMap = [];
-               foreach ( $sequences as $seq ) {
-                       if ( $seq['diffs'] === [] ) {
-                               continue;
-                       }
-                       if ( $tail === '' ) {
-                               $this->mDiffs[] = $seq['diffs'][0];
-                       } else {
-                               $head = $this->patch( '', $seq['diffs'][0] );
-                               $this->mDiffs[] = $this->diff( $tail, $head );
-                       }
-                       $this->mDiffMap[] = $seq['map'][0];
-                       $diffsCount = count( $seq['diffs'] );
-                       for ( $i = 1; $i < $diffsCount; $i++ ) {
-                               $this->mDiffs[] = $seq['diffs'][$i];
-                               $this->mDiffMap[] = $seq['map'][$i];
-                       }
-                       $tail = $seq['tail'];
-               }
-       }
-
-       /**
-        * @param string $t1
-        * @param string $t2
-        * @return string
-        */
-       function diff( $t1, $t2 ) {
-               # Need to do a null concatenation with warnings off, due to bugs in the current version of xdiff
-               # "String is not zero-terminated"
-               Wikimedia\suppressWarnings();
-               $diff = xdiff_string_rabdiff( $t1, $t2 ) . '';
-               Wikimedia\restoreWarnings();
-               return $diff;
-       }
-
-       /**
-        * @param string $base
-        * @param string $diff
-        * @return bool|string
-        */
-       function patch( $base, $diff ) {
-               if ( function_exists( 'xdiff_string_bpatch' ) ) {
-                       Wikimedia\suppressWarnings();
-                       $text = xdiff_string_bpatch( $base, $diff ) . '';
-                       Wikimedia\restoreWarnings();
-                       return $text;
-               }
-
-               # Pure PHP implementation
-
-               $header = unpack( 'Vofp/Vcsize', substr( $diff, 0, 8 ) );
-
-               # Check the checksum if hash extension is available
-               $ofp = $this->xdiffAdler32( $base );
-               if ( $ofp !== false && $ofp !== substr( $diff, 0, 4 ) ) {
-                       wfDebug( __METHOD__ . ": incorrect base checksum\n" );
-                       return false;
-               }
-               if ( $header['csize'] != strlen( $base ) ) {
-                       wfDebug( __METHOD__ . ": incorrect base length\n" );
-                       return false;
-               }
-
-               $p = 8;
-               $out = '';
-               while ( $p < strlen( $diff ) ) {
-                       $x = unpack( 'Cop', substr( $diff, $p, 1 ) );
-                       $op = $x['op'];
-                       ++$p;
-                       switch ( $op ) {
-                               case self::XDL_BDOP_INS:
-                                       $x = unpack( 'Csize', substr( $diff, $p, 1 ) );
-                                       $p++;
-                                       $out .= substr( $diff, $p, $x['size'] );
-                                       $p += $x['size'];
-                                       break;
-                               case self::XDL_BDOP_INSB:
-                                       $x = unpack( 'Vcsize', substr( $diff, $p, 4 ) );
-                                       $p += 4;
-                                       $out .= substr( $diff, $p, $x['csize'] );
-                                       $p += $x['csize'];
-                                       break;
-                               case self::XDL_BDOP_CPY:
-                                       $x = unpack( 'Voff/Vcsize', substr( $diff, $p, 8 ) );
-                                       $p += 8;
-                                       $out .= substr( $base, $x['off'], $x['csize'] );
-                                       break;
-                               default:
-                                       wfDebug( __METHOD__ . ": invalid op\n" );
-                                       return false;
-                       }
-               }
-               return $out;
-       }
-
-       /**
-        * Compute a binary "Adler-32" checksum as defined by LibXDiff, i.e. with
-        * the bytes backwards and initialised with 0 instead of 1. See T36428.
-        *
-        * @param string $s
-        * @return string|bool False if the hash extension is not available
-        */
-       function xdiffAdler32( $s ) {
-               if ( !function_exists( 'hash' ) ) {
-                       return false;
-               }
-
-               static $init;
-               if ( $init === null ) {
-                       $init = str_repeat( "\xf0", 205 ) . "\xee" . str_repeat( "\xf0", 67 ) . "\x02";
-               }
-
-               // The real Adler-32 checksum of $init is zero, so it initialises the
-               // state to zero, as it is at the start of LibXDiff's checksum
-               // algorithm. Appending the subject string then simulates LibXDiff.
-               return strrev( hash( 'adler32', $init . $s, true ) );
-       }
-
-       function uncompress() {
-               if ( !$this->mDiffs ) {
-                       return;
-               }
-               $tail = '';
-               $mDiffsCount = count( $this->mDiffs );
-               for ( $diffKey = 0; $diffKey < $mDiffsCount; $diffKey++ ) {
-                       $textKey = $this->mDiffMap[$diffKey];
-                       $text = $this->patch( $tail, $this->mDiffs[$diffKey] );
-                       $this->mItems[$textKey] = $text;
-                       $tail = $text;
-               }
-       }
-
-       /**
-        * @return array
-        */
-       function __sleep() {
-               $this->compress();
-               if ( $this->mItems === [] ) {
-                       $info = false;
-               } else {
-                       // Take forward differences to improve the compression ratio for sequences
-                       $map = '';
-                       $prev = 0;
-                       foreach ( $this->mDiffMap as $i ) {
-                               if ( $map !== '' ) {
-                                       $map .= ',';
-                               }
-                               $map .= $i - $prev;
-                               $prev = $i;
-                       }
-                       $info = [
-                               'diffs' => $this->mDiffs,
-                               'map' => $map
-                       ];
-               }
-               if ( isset( $this->mDefaultKey ) ) {
-                       $info['default'] = $this->mDefaultKey;
-               }
-               $this->mCompressed = gzdeflate( serialize( $info ) );
-               return [ 'mCompressed' ];
-       }
-
-       function __wakeup() {
-               // addItem() doesn't work if mItems is partially filled from mDiffs
-               $this->mFrozen = true;
-               $info = unserialize( gzinflate( $this->mCompressed ) );
-               unset( $this->mCompressed );
-
-               if ( !$info ) {
-                       // Empty object
-                       return;
-               }
-
-               if ( isset( $info['default'] ) ) {
-                       $this->mDefaultKey = $info['default'];
-               }
-               $this->mDiffs = $info['diffs'];
-               if ( isset( $info['base'] ) ) {
-                       // Old format
-                       $this->mDiffMap = range( 0, count( $this->mDiffs ) - 1 );
-                       array_unshift( $this->mDiffs,
-                               pack( 'VVCV', 0, 0, self::XDL_BDOP_INSB, strlen( $info['base'] ) ) .
-                               $info['base'] );
-               } else {
-                       // New format
-                       $map = explode( ',', $info['map'] );
-                       $cur = 0;
-                       $this->mDiffMap = [];
-                       foreach ( $map as $i ) {
-                               $cur += $i;
-                               $this->mDiffMap[] = $cur;
-                       }
-               }
-               $this->uncompress();
-       }
-
-       /**
-        * Helper function for compression jobs
-        * Returns true until the object is "full" and ready to be committed
-        *
-        * @return bool
-        */
-       function isHappy() {
-               return $this->mSize < $this->mMaxSize
-                       && count( $this->mItems ) < $this->mMaxCount;
-       }
-
-}
-
-// phpcs:ignore Generic.CodeAnalysis.UnconditionalIfStatement.Found
-if ( false ) {
-       // Blobs generated by MediaWiki < 1.5 on PHP 4 were serialized with the
-       // class name coerced to lowercase. We can improve efficiency by adding
-       // autoload entries for the lowercase variants of these classes (T166759).
-       // The code below is never executed, but it is picked up by the AutoloadGenerator
-       // parser, which scans for class_alias() calls.
-       class_alias( ConcatenatedGzipHistoryBlob::class, 'concatenatedgziphistoryblob' );
-       class_alias( HistoryBlobCurStub::class, 'historyblobcurstub' );
-       class_alias( HistoryBlobStub::class, 'historyblobstub' );
-}
index df99556..17dc037 100644 (file)
@@ -1765,15 +1765,7 @@ class Linker {
                        $inner = $context->msg( 'brackets' )->rawParams( $inner )->escaped();
                }
 
-               /**
-                * FIXME
-                * Remove all references to DisableRollbackConfirmationFeature
-                * after release of rollback feature. See T199534
-                */
-               if ( !MediaWikiServices::getInstance()
-                               ->getMainConfig()->get( 'DisableRollbackConfirmationFeature' ) &&
-                        $context->getUser()->getBoolOption( 'showrollbackconfirmation' )
-               ) {
+               if ( $context->getUser()->getBoolOption( 'showrollbackconfirmation' ) ) {
                        $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
                        $stats->increment( 'rollbackconfirmation.event.load' );
                        $context->getOutput()->addModules( 'mediawiki.page.rollback.confirmation' );
index bcec0a1..db5750a 100644 (file)
@@ -106,7 +106,7 @@ class MovePage {
 
                $oldid = $this->oldTitle->getArticleID();
 
-               if ( strlen( $this->newTitle->getDBkey() ) < 1 ) {
+               if ( $this->newTitle->getDBkey() === '' ) {
                        $status->fatal( 'articleexists' );
                }
                if (
index 7af80dc..b400797 100644 (file)
@@ -562,7 +562,7 @@ class SiteConfiguration {
                                ->execute();
 
                        $data = trim( $result->getStdout() );
-                       if ( $result->getExitCode() != 0 || !strlen( $data ) ) {
+                       if ( $result->getExitCode() || $data === '' ) {
                                throw new MWException( "Failed to run getConfiguration.php: {$result->getStdout()}" );
                        }
                        $res = unserialize( $data );
index 0f45839..ce0b959 100644 (file)
@@ -1702,16 +1702,18 @@ class Title implements LinkTarget, IDBAccessObject {
         * @return string Base name
         */
        public function getBaseText() {
+               $text = $this->getText();
                if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
-                       return $this->getText();
+                       return $text;
                }
 
-               $parts = explode( '/', $this->getText() );
-               # Don't discard the real title if there's no subpage involved
-               if ( count( $parts ) > 1 ) {
-                       unset( $parts[count( $parts ) - 1] );
+               $lastSlashPos = strrpos( $text, '/' );
+               // Don't discard the real title if there's no subpage involved
+               if ( $lastSlashPos === false ) {
+                       return $text;
                }
-               return implode( '/', $parts );
+
+               return substr( $text, 0, $lastSlashPos );
        }
 
        /**
index 0e86fda..e2fc265 100644 (file)
@@ -73,20 +73,12 @@ class RollbackAction extends FormAction {
        }
 
        /**
-        * @throws ConfigException
         * @throws ErrorPageError
         * @throws ReadOnlyError
         * @throws ThrottledError
         */
        public function show() {
-               /**
-                * FIXME
-                * Remove temporary check of DisableRollbackConfirmationFeature
-                * after release of rollback feature. See T199534
-                */
-               $config = \MediaWiki\MediaWikiServices::getInstance()->getMainConfig();
-               if ( $config->get( 'DisableRollbackConfirmationFeature' ) == true ||
-                        $this->getUser()->getOption( 'showrollbackconfirmation' ) == false ||
+               if ( $this->getUser()->getOption( 'showrollbackconfirmation' ) == false ||
                         $this->getRequest()->wasPosted() ) {
                        $this->handleRollbackRequest();
                } else {
index 96fa8a1..82a52b4 100644 (file)
@@ -664,8 +664,8 @@ class ApiQuerySiteinfo extends ApiQueryBase {
                }
 
                $data = [
-                       'url' => strlen( $url ) ? $url : '',
-                       'text' => strlen( $text ) ? $text : '',
+                       'url' => (string)$url,
+                       'text' => (string)$text,
                ];
 
                return $this->getResult()->addValue( 'query', $property, $data );
index fe5f6c4..5184562 100644 (file)
@@ -477,7 +477,7 @@ class ApiStashEdit extends ApiBase {
         * @param string $newKey
         */
        private static function pruneExcessStashedEntries( BagOStuff $cache, User $user, $newKey ) {
-               $key = $cache->makeKey( 'stash-edit-recent', $user->getId() );
+               $key = $cache->makeKey( 'stash-edit-recent', sha1( $user->getName() ) );
 
                $keyList = $cache->get( $key ) ?: [];
                if ( count( $keyList ) >= self::MAX_CACHE_RECENT ) {
index 0e03e72..2f3a6f6 100644 (file)
@@ -154,7 +154,7 @@ class WikiTextStructure {
                        'enableSectionEditTokens' => false,
                        'allowTOC' => false,
                ] );
-               if ( strlen( $text ) == 0 ) {
+               if ( $text === '' ) {
                        $this->allText = "";
                        // empty text - nothing to seek here
                        return;
diff --git a/includes/historyblob/ConcatenatedGzipHistoryBlob.php b/includes/historyblob/ConcatenatedGzipHistoryBlob.php
new file mode 100644 (file)
index 0000000..f6ca2f5
--- /dev/null
@@ -0,0 +1,146 @@
+<?php
+/**
+ * Efficient concatenated text storage.
+ *
+ * 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
+ */
+
+/**
+ * Concatenated gzip (CGZ) storage
+ * Improves compression ratio by concatenating like objects before gzipping
+ */
+class ConcatenatedGzipHistoryBlob implements HistoryBlob {
+       public $mVersion = 0, $mCompressed = false, $mItems = [], $mDefaultHash = '';
+       public $mSize = 0;
+       public $mMaxSize = 10000000;
+       public $mMaxCount = 100;
+
+       public function __construct() {
+               if ( !function_exists( 'gzdeflate' ) ) {
+                       throw new MWException( "Need zlib support to read or write this "
+                               . "kind of history object (ConcatenatedGzipHistoryBlob)\n" );
+               }
+       }
+
+       /**
+        * @param string $text
+        * @return string
+        */
+       public function addItem( $text ) {
+               $this->uncompress();
+               $hash = md5( $text );
+               if ( !isset( $this->mItems[$hash] ) ) {
+                       $this->mItems[$hash] = $text;
+                       $this->mSize += strlen( $text );
+               }
+               return $hash;
+       }
+
+       /**
+        * @param string $hash
+        * @return array|bool
+        */
+       public function getItem( $hash ) {
+               $this->uncompress();
+               if ( array_key_exists( $hash, $this->mItems ) ) {
+                       return $this->mItems[$hash];
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * @param string $text
+        * @return void
+        */
+       public function setText( $text ) {
+               $this->uncompress();
+               $this->mDefaultHash = $this->addItem( $text );
+       }
+
+       /**
+        * @return array|bool
+        */
+       public function getText() {
+               $this->uncompress();
+               return $this->getItem( $this->mDefaultHash );
+       }
+
+       /**
+        * Remove an item
+        *
+        * @param string $hash
+        */
+       public function removeItem( $hash ) {
+               $this->mSize -= strlen( $this->mItems[$hash] );
+               unset( $this->mItems[$hash] );
+       }
+
+       /**
+        * Compress the bulk data in the object
+        */
+       public function compress() {
+               if ( !$this->mCompressed ) {
+                       $this->mItems = gzdeflate( serialize( $this->mItems ) );
+                       $this->mCompressed = true;
+               }
+       }
+
+       /**
+        * Uncompress bulk data
+        */
+       public function uncompress() {
+               if ( $this->mCompressed ) {
+                       $this->mItems = unserialize( gzinflate( $this->mItems ) );
+                       $this->mCompressed = false;
+               }
+       }
+
+       /**
+        * @return array
+        */
+       function __sleep() {
+               $this->compress();
+               return [ 'mVersion', 'mCompressed', 'mItems', 'mDefaultHash' ];
+       }
+
+       function __wakeup() {
+               $this->uncompress();
+       }
+
+       /**
+        * Helper function for compression jobs
+        * Returns true until the object is "full" and ready to be committed
+        *
+        * @return bool
+        */
+       public function isHappy() {
+               return $this->mSize < $this->mMaxSize
+                       && count( $this->mItems ) < $this->mMaxCount;
+       }
+}
+
+// phpcs:ignore Generic.CodeAnalysis.UnconditionalIfStatement.Found
+if ( false ) {
+       // Blobs generated by MediaWiki < 1.5 on PHP 4 were serialized with the
+       // class name coerced to lowercase. We can improve efficiency by adding
+       // autoload entries for the lowercase variants of these classes (T166759).
+       // The code below is never executed, but it is picked up by the AutoloadGenerator
+       // parser, which scans for class_alias() calls.
+       class_alias( ConcatenatedGzipHistoryBlob::class, 'concatenatedgziphistoryblob' );
+}
diff --git a/includes/historyblob/DiffHistoryBlob.php b/includes/historyblob/DiffHistoryBlob.php
new file mode 100644 (file)
index 0000000..8d92fe5
--- /dev/null
@@ -0,0 +1,377 @@
+<?php
+/**
+ * Efficient concatenated text storage.
+ *
+ * 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
+ */
+
+/**
+ * Diff-based history compression
+ * Requires xdiff 1.5+ and zlib
+ */
+class DiffHistoryBlob implements HistoryBlob {
+       /** @var array Uncompressed item cache */
+       public $mItems = [];
+
+       /** @var int Total uncompressed size */
+       public $mSize = 0;
+
+       /**
+        * @var array Array of diffs. If a diff D from A to B is notated D = B - A,
+        * and Z is an empty string:
+        *
+        *              { item[map[i]] - item[map[i-1]]   where i > 0
+        *    diff[i] = {
+        *              { item[map[i]] - Z                where i = 0
+        */
+       public $mDiffs;
+
+       /** @var array The diff map, see above */
+       public $mDiffMap;
+
+       /** @var int The key for getText()
+        */
+       public $mDefaultKey;
+
+       /** @var string Compressed storage */
+       public $mCompressed;
+
+       /** @var bool True if the object is locked against further writes */
+       public $mFrozen = false;
+
+       /**
+        * @var int The maximum uncompressed size before the object becomes sad
+        * Should be less than max_allowed_packet
+        */
+       public $mMaxSize = 10000000;
+
+       /** @var int The maximum number of text items before the object becomes sad */
+       public $mMaxCount = 100;
+
+       /** Constants from xdiff.h */
+       const XDL_BDOP_INS = 1;
+       const XDL_BDOP_CPY = 2;
+       const XDL_BDOP_INSB = 3;
+
+       function __construct() {
+               if ( !function_exists( 'gzdeflate' ) ) {
+                       throw new MWException( "Need zlib support to read or write DiffHistoryBlob\n" );
+               }
+       }
+
+       /**
+        * @throws MWException
+        * @param string $text
+        * @return int
+        */
+       function addItem( $text ) {
+               if ( $this->mFrozen ) {
+                       throw new MWException( __METHOD__ . ": Cannot add more items after sleep/wakeup" );
+               }
+
+               $this->mItems[] = $text;
+               $this->mSize += strlen( $text );
+               $this->mDiffs = null; // later
+               return count( $this->mItems ) - 1;
+       }
+
+       /**
+        * @param string $key
+        * @return string
+        */
+       function getItem( $key ) {
+               return $this->mItems[$key];
+       }
+
+       /**
+        * @param string $text
+        */
+       function setText( $text ) {
+               $this->mDefaultKey = $this->addItem( $text );
+       }
+
+       /**
+        * @return string
+        */
+       function getText() {
+               return $this->getItem( $this->mDefaultKey );
+       }
+
+       /**
+        * @throws MWException
+        */
+       function compress() {
+               if ( !function_exists( 'xdiff_string_rabdiff' ) ) {
+                       throw new MWException( "Need xdiff 1.5+ support to write DiffHistoryBlob\n" );
+               }
+               if ( isset( $this->mDiffs ) ) {
+                       // Already compressed
+                       return;
+               }
+               if ( $this->mItems === [] ) {
+                       return;
+               }
+
+               // Create two diff sequences: one for main text and one for small text
+               $sequences = [
+                       'small' => [
+                               'tail' => '',
+                               'diffs' => [],
+                               'map' => [],
+                       ],
+                       'main' => [
+                               'tail' => '',
+                               'diffs' => [],
+                               'map' => [],
+                       ],
+               ];
+               $smallFactor = 0.5;
+
+               $mItemsCount = count( $this->mItems );
+               for ( $i = 0; $i < $mItemsCount; $i++ ) {
+                       $text = $this->mItems[$i];
+                       if ( $i == 0 ) {
+                               $seqName = 'main';
+                       } else {
+                               $mainTail = $sequences['main']['tail'];
+                               if ( strlen( $text ) < strlen( $mainTail ) * $smallFactor ) {
+                                       $seqName = 'small';
+                               } else {
+                                       $seqName = 'main';
+                               }
+                       }
+                       $seq =& $sequences[$seqName];
+                       $tail = $seq['tail'];
+                       $diff = $this->diff( $tail, $text );
+                       $seq['diffs'][] = $diff;
+                       $seq['map'][] = $i;
+                       $seq['tail'] = $text;
+               }
+               unset( $seq ); // unlink dangerous alias
+
+               // Knit the sequences together
+               $tail = '';
+               $this->mDiffs = [];
+               $this->mDiffMap = [];
+               foreach ( $sequences as $seq ) {
+                       if ( $seq['diffs'] === [] ) {
+                               continue;
+                       }
+                       if ( $tail === '' ) {
+                               $this->mDiffs[] = $seq['diffs'][0];
+                       } else {
+                               $head = $this->patch( '', $seq['diffs'][0] );
+                               $this->mDiffs[] = $this->diff( $tail, $head );
+                       }
+                       $this->mDiffMap[] = $seq['map'][0];
+                       $diffsCount = count( $seq['diffs'] );
+                       for ( $i = 1; $i < $diffsCount; $i++ ) {
+                               $this->mDiffs[] = $seq['diffs'][$i];
+                               $this->mDiffMap[] = $seq['map'][$i];
+                       }
+                       $tail = $seq['tail'];
+               }
+       }
+
+       /**
+        * @param string $t1
+        * @param string $t2
+        * @return string
+        */
+       function diff( $t1, $t2 ) {
+               # Need to do a null concatenation with warnings off, due to bugs in the current version of xdiff
+               # "String is not zero-terminated"
+               Wikimedia\suppressWarnings();
+               $diff = xdiff_string_rabdiff( $t1, $t2 ) . '';
+               Wikimedia\restoreWarnings();
+               return $diff;
+       }
+
+       /**
+        * @param string $base
+        * @param string $diff
+        * @return bool|string
+        */
+       function patch( $base, $diff ) {
+               if ( function_exists( 'xdiff_string_bpatch' ) ) {
+                       Wikimedia\suppressWarnings();
+                       $text = xdiff_string_bpatch( $base, $diff ) . '';
+                       Wikimedia\restoreWarnings();
+                       return $text;
+               }
+
+               # Pure PHP implementation
+
+               $header = unpack( 'Vofp/Vcsize', substr( $diff, 0, 8 ) );
+
+               # Check the checksum if hash extension is available
+               $ofp = $this->xdiffAdler32( $base );
+               if ( $ofp !== false && $ofp !== substr( $diff, 0, 4 ) ) {
+                       wfDebug( __METHOD__ . ": incorrect base checksum\n" );
+                       return false;
+               }
+               if ( $header['csize'] != strlen( $base ) ) {
+                       wfDebug( __METHOD__ . ": incorrect base length\n" );
+                       return false;
+               }
+
+               $p = 8;
+               $out = '';
+               while ( $p < strlen( $diff ) ) {
+                       $x = unpack( 'Cop', substr( $diff, $p, 1 ) );
+                       $op = $x['op'];
+                       ++$p;
+                       switch ( $op ) {
+                               case self::XDL_BDOP_INS:
+                                       $x = unpack( 'Csize', substr( $diff, $p, 1 ) );
+                                       $p++;
+                                       $out .= substr( $diff, $p, $x['size'] );
+                                       $p += $x['size'];
+                                       break;
+                               case self::XDL_BDOP_INSB:
+                                       $x = unpack( 'Vcsize', substr( $diff, $p, 4 ) );
+                                       $p += 4;
+                                       $out .= substr( $diff, $p, $x['csize'] );
+                                       $p += $x['csize'];
+                                       break;
+                               case self::XDL_BDOP_CPY:
+                                       $x = unpack( 'Voff/Vcsize', substr( $diff, $p, 8 ) );
+                                       $p += 8;
+                                       $out .= substr( $base, $x['off'], $x['csize'] );
+                                       break;
+                               default:
+                                       wfDebug( __METHOD__ . ": invalid op\n" );
+                                       return false;
+                       }
+               }
+               return $out;
+       }
+
+       /**
+        * Compute a binary "Adler-32" checksum as defined by LibXDiff, i.e. with
+        * the bytes backwards and initialised with 0 instead of 1. See T36428.
+        *
+        * @param string $s
+        * @return string|bool False if the hash extension is not available
+        */
+       function xdiffAdler32( $s ) {
+               if ( !function_exists( 'hash' ) ) {
+                       return false;
+               }
+
+               static $init;
+               if ( $init === null ) {
+                       $init = str_repeat( "\xf0", 205 ) . "\xee" . str_repeat( "\xf0", 67 ) . "\x02";
+               }
+
+               // The real Adler-32 checksum of $init is zero, so it initialises the
+               // state to zero, as it is at the start of LibXDiff's checksum
+               // algorithm. Appending the subject string then simulates LibXDiff.
+               return strrev( hash( 'adler32', $init . $s, true ) );
+       }
+
+       function uncompress() {
+               if ( !$this->mDiffs ) {
+                       return;
+               }
+               $tail = '';
+               $mDiffsCount = count( $this->mDiffs );
+               for ( $diffKey = 0; $diffKey < $mDiffsCount; $diffKey++ ) {
+                       $textKey = $this->mDiffMap[$diffKey];
+                       $text = $this->patch( $tail, $this->mDiffs[$diffKey] );
+                       $this->mItems[$textKey] = $text;
+                       $tail = $text;
+               }
+       }
+
+       /**
+        * @return array
+        */
+       function __sleep() {
+               $this->compress();
+               if ( $this->mItems === [] ) {
+                       $info = false;
+               } else {
+                       // Take forward differences to improve the compression ratio for sequences
+                       $map = '';
+                       $prev = 0;
+                       foreach ( $this->mDiffMap as $i ) {
+                               if ( $map !== '' ) {
+                                       $map .= ',';
+                               }
+                               $map .= $i - $prev;
+                               $prev = $i;
+                       }
+                       $info = [
+                               'diffs' => $this->mDiffs,
+                               'map' => $map
+                       ];
+               }
+               if ( isset( $this->mDefaultKey ) ) {
+                       $info['default'] = $this->mDefaultKey;
+               }
+               $this->mCompressed = gzdeflate( serialize( $info ) );
+               return [ 'mCompressed' ];
+       }
+
+       function __wakeup() {
+               // addItem() doesn't work if mItems is partially filled from mDiffs
+               $this->mFrozen = true;
+               $info = unserialize( gzinflate( $this->mCompressed ) );
+               unset( $this->mCompressed );
+
+               if ( !$info ) {
+                       // Empty object
+                       return;
+               }
+
+               if ( isset( $info['default'] ) ) {
+                       $this->mDefaultKey = $info['default'];
+               }
+               $this->mDiffs = $info['diffs'];
+               if ( isset( $info['base'] ) ) {
+                       // Old format
+                       $this->mDiffMap = range( 0, count( $this->mDiffs ) - 1 );
+                       array_unshift( $this->mDiffs,
+                               pack( 'VVCV', 0, 0, self::XDL_BDOP_INSB, strlen( $info['base'] ) ) .
+                               $info['base'] );
+               } else {
+                       // New format
+                       $map = explode( ',', $info['map'] );
+                       $cur = 0;
+                       $this->mDiffMap = [];
+                       foreach ( $map as $i ) {
+                               $cur += $i;
+                               $this->mDiffMap[] = $cur;
+                       }
+               }
+               $this->uncompress();
+       }
+
+       /**
+        * Helper function for compression jobs
+        * Returns true until the object is "full" and ready to be committed
+        *
+        * @return bool
+        */
+       function isHappy() {
+               return $this->mSize < $this->mMaxSize
+                       && count( $this->mItems ) < $this->mMaxCount;
+       }
+
+}
diff --git a/includes/historyblob/HistoryBlob.php b/includes/historyblob/HistoryBlob.php
new file mode 100644 (file)
index 0000000..36c7c8f
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+/**
+ * Efficient concatenated text storage.
+ *
+ * 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
+ */
+
+/**
+ * Base class for general text storage via the "object" flag in old_flags, or
+ * two-part external storage URLs. Used for represent efficient concatenated
+ * storage, and migration-related pointer objects.
+ */
+interface HistoryBlob {
+       /**
+        * Adds an item of text, returns a stub object which points to the item.
+        * You must call setLocation() on the stub object before storing it to the
+        * database
+        *
+        * @param string $text
+        *
+        * @return string The key for getItem()
+        */
+       function addItem( $text );
+
+       /**
+        * Get item by key, or false if the key is not present
+        *
+        * @param string $key
+        *
+        * @return string|bool
+        */
+       function getItem( $key );
+
+       /**
+        * Set the "default text"
+        * This concept is an odd property of the current DB schema, whereby each text item has a revision
+        * associated with it. The default text is the text of the associated revision. There may, however,
+        * be other revisions in the same object.
+        *
+        * Default text is not required for two-part external storage URLs.
+        *
+        * @param string $text
+        */
+       function setText( $text );
+
+       /**
+        * Get default text. This is called from Revision::getRevisionText()
+        *
+        * @return string
+        */
+       function getText();
+}
diff --git a/includes/historyblob/HistoryBlobCurStub.php b/includes/historyblob/HistoryBlobCurStub.php
new file mode 100644 (file)
index 0000000..8858c8d
--- /dev/null
@@ -0,0 +1,73 @@
+<?php
+/**
+ * Efficient concatenated text storage.
+ *
+ * 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
+ */
+
+/**
+ * To speed up conversion from 1.4 to 1.5 schema, text rows can refer to the
+ * leftover cur table as the backend. This avoids expensively copying hundreds
+ * of megabytes of data during the conversion downtime.
+ *
+ * Serialized HistoryBlobCurStub objects will be inserted into the text table
+ * on conversion if $wgLegacySchemaConversion is set to true.
+ */
+class HistoryBlobCurStub {
+       /** @var int */
+       public $mCurId;
+
+       /**
+        * @param int $curid The cur_id pointed to
+        */
+       function __construct( $curid = 0 ) {
+               $this->mCurId = $curid;
+       }
+
+       /**
+        * Sets the location (cur_id) of the main object to which this object
+        * points
+        *
+        * @param int $id
+        */
+       function setLocation( $id ) {
+               $this->mCurId = $id;
+       }
+
+       /**
+        * @return string|bool
+        */
+       function getText() {
+               $dbr = wfGetDB( DB_REPLICA );
+               $row = $dbr->selectRow( 'cur', [ 'cur_text' ], [ 'cur_id' => $this->mCurId ] );
+               if ( !$row ) {
+                       return false;
+               }
+               return $row->cur_text;
+       }
+}
+
+// phpcs:ignore Generic.CodeAnalysis.UnconditionalIfStatement.Found
+if ( false ) {
+       // Blobs generated by MediaWiki < 1.5 on PHP 4 were serialized with the
+       // class name coerced to lowercase. We can improve efficiency by adding
+       // autoload entries for the lowercase variants of these classes (T166759).
+       // The code below is never executed, but it is picked up by the AutoloadGenerator
+       // parser, which scans for class_alias() calls.
+       class_alias( HistoryBlobCurStub::class, 'historyblobcurstub' );
+}
diff --git a/includes/historyblob/HistoryBlobStub.php b/includes/historyblob/HistoryBlobStub.php
new file mode 100644 (file)
index 0000000..4995d3b
--- /dev/null
@@ -0,0 +1,150 @@
+<?php
+/**
+ * Efficient concatenated text storage.
+ *
+ * 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
+ */
+
+/**
+ * Pointer object for an item within a CGZ blob stored in the text table.
+ */
+class HistoryBlobStub {
+       /**
+        * @var array One-step cache variable to hold base blobs; operations that
+        * pull multiple revisions may often pull multiple times from the same
+        * blob. By keeping the last-used one open, we avoid redundant
+        * unserialization and decompression overhead.
+        */
+       protected static $blobCache = [];
+
+       /** @var int */
+       public $mOldId;
+
+       /** @var string */
+       public $mHash;
+
+       /** @var string */
+       public $mRef;
+
+       /**
+        * @param string $hash The content hash of the text
+        * @param int $oldid The old_id for the CGZ object
+        */
+       function __construct( $hash = '', $oldid = 0 ) {
+               $this->mHash = $hash;
+       }
+
+       /**
+        * Sets the location (old_id) of the main object to which this object
+        * points
+        * @param int $id
+        */
+       function setLocation( $id ) {
+               $this->mOldId = $id;
+       }
+
+       /**
+        * Sets the location (old_id) of the referring object
+        * @param string $id
+        */
+       function setReferrer( $id ) {
+               $this->mRef = $id;
+       }
+
+       /**
+        * Gets the location of the referring object
+        * @return string
+        */
+       function getReferrer() {
+               return $this->mRef;
+       }
+
+       /**
+        * @return string|false
+        */
+       function getText() {
+               if ( isset( self::$blobCache[$this->mOldId] ) ) {
+                       $obj = self::$blobCache[$this->mOldId];
+               } else {
+                       $dbr = wfGetDB( DB_REPLICA );
+                       $row = $dbr->selectRow(
+                               'text',
+                               [ 'old_flags', 'old_text' ],
+                               [ 'old_id' => $this->mOldId ]
+                       );
+
+                       if ( !$row ) {
+                               return false;
+                       }
+
+                       $flags = explode( ',', $row->old_flags );
+                       if ( in_array( 'external', $flags ) ) {
+                               $url = $row->old_text;
+                               $parts = explode( '://', $url, 2 );
+                               if ( !isset( $parts[1] ) || $parts[1] == '' ) {
+                                       return false;
+                               }
+                               $row->old_text = ExternalStore::fetchFromURL( $url );
+
+                       }
+
+                       if ( !in_array( 'object', $flags ) ) {
+                               return false;
+                       }
+
+                       if ( in_array( 'gzip', $flags ) ) {
+                               // This shouldn't happen, but a bug in the compress script
+                               // may at times gzip-compress a HistoryBlob object row.
+                               $obj = unserialize( gzinflate( $row->old_text ) );
+                       } else {
+                               $obj = unserialize( $row->old_text );
+                       }
+
+                       if ( !is_object( $obj ) ) {
+                               // Correct for old double-serialization bug.
+                               $obj = unserialize( $obj );
+                       }
+
+                       // Save this item for reference; if pulling many
+                       // items in a row we'll likely use it again.
+                       $obj->uncompress();
+                       self::$blobCache = [ $this->mOldId => $obj ];
+               }
+
+               return $obj->getItem( $this->mHash );
+       }
+
+       /**
+        * Get the content hash
+        *
+        * @return string
+        */
+       function getHash() {
+               return $this->mHash;
+       }
+}
+
+// phpcs:ignore Generic.CodeAnalysis.UnconditionalIfStatement.Found
+if ( false ) {
+       // Blobs generated by MediaWiki < 1.5 on PHP 4 were serialized with the
+       // class name coerced to lowercase. We can improve efficiency by adding
+       // autoload entries for the lowercase variants of these classes (T166759).
+       // The code below is never executed, but it is picked up by the AutoloadGenerator
+       // parser, which scans for class_alias() calls.
+       class_alias( HistoryBlobStub::class, 'historyblobstub' );
+}
index 30ab181..d2af8c8 100644 (file)
@@ -217,7 +217,7 @@ class PhpHttpRequest extends MWHttpRequest {
                                        break;
                                }
 
-                               if ( strlen( $buf ) ) {
+                               if ( $buf !== '' ) {
                                        call_user_func( $this->callback, $fh, $buf );
                                }
                        }
index 63575eb..eb8b1a2 100644 (file)
@@ -93,7 +93,7 @@ class ThumbnailRenderJob extends Job {
                if ( $wgUploadThumbnailRenderHttpCustomDomain ) {
                        $parsedUrl = wfParseUrl( $thumbUrl );
 
-                       if ( !$parsedUrl || !isset( $parsedUrl['path'] ) || !strlen( $parsedUrl['path'] ) ) {
+                       if ( !isset( $parsedUrl['path'] ) || $parsedUrl['path'] === '' ) {
                                $this->setLastError( __METHOD__ . ": invalid thumb URL: $thumbUrl" );
                                return false;
                        }
index 5669366..0dd7b57 100644 (file)
@@ -288,13 +288,10 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
         */
        protected function mergeViaCas( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
                do {
-                       $this->clearLastError();
-                       $reportDupes = $this->reportDupes;
-                       $this->reportDupes = false;
                        $casToken = null; // passed by reference
+                       // Get the old value and CAS token from cache
+                       $this->clearLastError();
                        $currentValue = $this->doGet( $key, self::READ_LATEST, $casToken );
-                       $this->reportDupes = $reportDupes;
-
                        if ( $this->getLastError() ) {
                                $this->logger->warning(
                                        __METHOD__ . ' failed due to I/O error on get() for {key}.',
index 87bccc5..9f9cc3c 100644 (file)
@@ -194,8 +194,8 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
        /** Tiny positive float to use when using "minTime" to assert an inequality */
        const TINY_POSTIVE = 0.000001;
 
-       /** Seconds of delay after get() where set() storms are a consideration with 'lockTSE' */
-       const SET_DELAY_HIGH_SEC = 0.1;
+       /** Milliseconds of delay after get() where set() storms are a consideration with 'lockTSE' */
+       const SET_DELAY_HIGH_MS = 50;
        /** Min millisecond set() backoff for keys in hold-off (far less than INTERIM_KEY_TTL) */
        const RECENT_SET_LOW_MS = 50;
        /** Max millisecond set() backoff for keys in hold-off (far less than INTERIM_KEY_TTL) */
@@ -521,31 +521,34 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         * @param int $ttl Seconds to live. Special values are:
         *   - WANObjectCache::TTL_INDEFINITE: Cache forever (default)
         * @param array $opts Options map:
-        *   - lag : Seconds of replica DB lag. Typically, this is either the replica DB lag
+        *   - lag: seconds of replica DB lag. Typically, this is either the replica DB lag
         *      before the data was read or, if applicable, the replica DB lag before
         *      the snapshot-isolated transaction the data was read from started.
         *      Use false to indicate that replication is not running.
         *      Default: 0 seconds
-        *   - since : UNIX timestamp of the data in $value. Typically, this is either
+        *   - since: UNIX timestamp of the data in $value. Typically, this is either
         *      the current time the data was read or (if applicable) the time when
         *      the snapshot-isolated transaction the data was read from started.
         *      Default: 0 seconds
-        *   - pending : Whether this data is possibly from an uncommitted write transaction.
+        *   - pending: whether this data is possibly from an uncommitted write transaction.
         *      Generally, other threads should not see values from the future and
         *      they certainly should not see ones that ended up getting rolled back.
         *      Default: false
-        *   - lockTSE : if excessive replication/snapshot lag is detected, then store the value
+        *   - lockTSE: if excessive replication/snapshot lag is detected, then store the value
         *      with this TTL and flag it as stale. This is only useful if the reads for this key
         *      use getWithSetCallback() with "lockTSE" set. Note that if "staleTTL" is set
         *      then it will still add on to this TTL in the excessive lag scenario.
         *      Default: WANObjectCache::TSE_NONE
-        *   - staleTTL : Seconds to keep the key around if it is stale. The get()/getMulti()
+        *   - staleTTL: seconds to keep the key around if it is stale. The get()/getMulti()
         *      methods return such stale values with a $curTTL of 0, and getWithSetCallback()
         *      will call the regeneration callback in such cases, passing in the old value
         *      and its as-of time to the callback. This is useful if adaptiveTTL() is used
         *      on the old value's as-of time when it is verified as still being correct.
         *      Default: WANObjectCache::STALE_TTL_NONE.
+        *   - creating: optimize for the case where the key does not already exist.
+        *      Default: false
         * @note Options added in 1.28: staleTTL
+        * @note Options added in 1.33: creating
         * @return bool Success
         */
        final public function set( $key, $value, $ttl = self::TTL_INDEFINITE, array $opts = [] ) {
@@ -553,6 +556,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                $lockTSE = $opts['lockTSE'] ?? self::TSE_NONE;
                $staleTTL = $opts['staleTTL'] ?? self::STALE_TTL_NONE;
                $age = isset( $opts['since'] ) ? max( 0, $now - $opts['since'] ) : 0;
+               $creating = $opts['creating'] ?? false;
                $lag = $opts['lag'] ?? 0;
 
                // Do not cache potentially uncommitted data as it might get rolled back
@@ -618,14 +622,23 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
 
                // Wrap that value with time/TTL/version metadata
                $wrapped = $this->wrap( $value, $logicalTTL ?: $ttl, $now );
+               $storeTTL = $ttl + $staleTTL;
 
-               $func = function ( $cache, $key, $cWrapped ) use ( $wrapped ) {
-                       return ( is_string( $cWrapped ) )
-                               ? false // key is tombstoned; do nothing
-                               : $wrapped;
-               };
+               if ( $creating ) {
+                       $ok = $this->cache->add( self::VALUE_KEY_PREFIX . $key, $wrapped, $storeTTL );
+               } else {
+                       $ok = $this->cache->merge(
+                               self::VALUE_KEY_PREFIX . $key,
+                               function ( $cache, $key, $cWrapped ) use ( $wrapped ) {
+                                       // A string value means that it is a tombstone; do nothing in that case
+                                       return ( is_string( $cWrapped ) ) ? false : $wrapped;
+                               },
+                               $storeTTL,
+                               1 // 1 attempt
+                       );
+               }
 
-               return $this->cache->merge( self::VALUE_KEY_PREFIX . $key, $func, $ttl + $staleTTL, 1 );
+               return $ok;
        }
 
        /**
@@ -1124,7 +1137,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         *      stampede is worth avoiding. Note that if the key falls out of cache then concurrent
         *      threads will all run the callback on cache miss until the value is saved in cache.
         *      The only stampede protection in that case is from duplicate cache sets when the
-        *      callback takes longer than WANObjectCache::SET_DELAY_HIGH_SEC seconds; consider
+        *      callback takes longer than WANObjectCache::SET_DELAY_HIGH_MS milliseconds; consider
         *      using "busyValue" if such stampedes are a problem. Note that the higher "lockTSE" is
         *      set, the higher the worst-case staleness of returned values can be. Also note that
         *      this option does not by itself handle the case of the key simply expiring on account
@@ -1418,6 +1431,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                                }
                        } elseif ( !$useMutex || $hasLock ) {
                                if ( $this->checkAndSetCooloff( $key, $kClass, $ago, $lockTSE, $hasLock ) ) {
+                                       $setOpts['creating'] = ( $curValue === false );
                                        // Save the value unless a lock-winning thread is already expected to do that
                                        $setOpts['lockTSE'] = $lockTSE;
                                        $setOpts['staleTTL'] = $staleTTL;
@@ -1464,7 +1478,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                // consistent hashing).
                if ( $lockTSE < 0 || $hasLock ) {
                        return true; // either not a priori hot or thread has the lock
-               } elseif ( $elapsed <= self::SET_DELAY_HIGH_SEC ) {
+               } elseif ( $elapsed <= self::SET_DELAY_HIGH_MS * 1e3 ) {
                        return true; // not enough time for threads to pile up
                }
 
index 8d82854..ca57938 100644 (file)
@@ -42,11 +42,11 @@ class DatabaseDomain {
         * @param string $prefix Table prefix
         */
        public function __construct( $database, $schema, $prefix ) {
-               if ( $database !== null && ( !is_string( $database ) || !strlen( $database ) ) ) {
+               if ( $database !== null && ( !is_string( $database ) || $database === '' ) ) {
                        throw new InvalidArgumentException( 'Database must be null or a non-empty string.' );
                }
                $this->database = $database;
-               if ( $schema !== null && ( !is_string( $schema ) || !strlen( $schema ) ) ) {
+               if ( $schema !== null && ( !is_string( $schema ) || $schema === '' ) ) {
                        throw new InvalidArgumentException( 'Schema must be null or a non-empty string.' );
                }
                $this->schema = $schema;
index b2f5342..a42726f 100644 (file)
@@ -870,17 +870,6 @@ class DefaultPreferencesFactory implements PreferencesFactory {
                                'section' => 'rendering/advancedrendering',
                                'label-message' => 'tog-showrollbackconfirmation',
                        ];
-
-                       /**
-                        * FIXME
-                        * Remove temporary help text and references to DisableRollbackConfirmationFeature
-                        * after release of rollback feature. See T199534
-                        */
-                       if ( MediaWikiServices::getInstance()
-                               ->getMainConfig()->get( 'DisableRollbackConfirmationFeature' ) ) {
-                               $defaultPreferences['showrollbackconfirmation']
-                               ['help-message'] = 'tog-showrollbackconfirmation-prerelease-warning';
-                       }
                }
        }
 
index a8271ac..82bc84d 100644 (file)
@@ -616,9 +616,7 @@ abstract class ChangesListSpecialPage extends SpecialPage {
        }
 
        /**
-        * Main execution point
-        *
-        * @param string $subpage
+        * @param string|null $subpage
         */
        public function execute( $subpage ) {
                $this->rcSubpage = $subpage;
index d1c6aea..939460f 100644 (file)
@@ -31,7 +31,7 @@
 abstract class FormSpecialPage extends SpecialPage {
        /**
         * The sub-page of the special page.
-        * @var string
+        * @var string|null
         */
        protected $par = null;
 
@@ -166,7 +166,7 @@ abstract class FormSpecialPage extends SpecialPage {
        /**
         * Basic SpecialPage workflow: get a form, send it to the user; get some data back,
         *
-        * @param string $par Subpage string if one was specified
+        * @param string|null $par Subpage string if one was specified
         */
        public function execute( $par ) {
                $this->setParameter( $par );
@@ -188,7 +188,7 @@ abstract class FormSpecialPage extends SpecialPage {
 
        /**
         * Maybe do something interesting with the subpage parameter
-        * @param string $par
+        * @param string|null $par
         */
        protected function setParameter( $par ) {
                $this->par = $par;
index b88479a..f0cb7e5 100644 (file)
@@ -578,7 +578,7 @@ abstract class QueryPage extends SpecialPage {
        /**
         * This is the actual workhorse. It does everything needed to make a
         * real, honest-to-gosh query page.
-        * @param string $par
+        * @param string|null $par
         */
        public function execute( $par ) {
                $user = $this->getUser();
index 0c709af..f52a6f3 100644 (file)
@@ -33,9 +33,7 @@ class SpecialActiveUsers extends SpecialPage {
        }
 
        /**
-        * Show the special page
-        *
-        * @param string $par Parameter passed to the page or null
+        * @param string|null $par Parameter passed to the page or null
         */
        public function execute( $par ) {
                $out = $this->getOutput();
index 2482d74..878440d 100644 (file)
@@ -35,9 +35,7 @@ class SpecialAllMessages extends SpecialPage {
        }
 
        /**
-        * Show the special page
-        *
-        * @param string $par Parameter passed to the page or null
+        * @param string|null $par Parameter passed to the page or null
         */
        public function execute( $par ) {
                $out = $this->getOutput();
index cab5a2e..34c3371 100644 (file)
@@ -34,9 +34,7 @@ class SpecialAutoblockList extends SpecialPage {
        }
 
        /**
-        * Main execution point
-        *
-        * @param string $par Title fragment
+        * @param string|null $par Title fragment
         */
        public function execute( $par ) {
                $this->setHeaders();
index 186e5ad..fd27fdc 100644 (file)
@@ -36,9 +36,7 @@ class SpecialBlockList extends SpecialPage {
        }
 
        /**
-        * Main execution point
-        *
-        * @param string $par Title fragment
+        * @param string|null $par Title fragment
         */
        public function execute( $par ) {
                $this->setHeaders();
index 2fe38ed..ea9ddaf 100644 (file)
@@ -36,9 +36,7 @@ class SpecialBookSources extends SpecialPage {
        }
 
        /**
-        * Show the special page
-        *
-        * @param string $isbn ISBN passed as a subpage parameter
+        * @param string|null $isbn ISBN passed as a subpage parameter
         */
        public function execute( $isbn ) {
                $out = $this->getOutput();
index d6fb10f..9d1b79e 100644 (file)
@@ -43,7 +43,7 @@ class SpecialComparePages extends SpecialPage {
        /**
         * Show a form for filtering namespace and username
         *
-        * @param string $par
+        * @param string|null $par
         * @return string
         */
        public function execute( $par ) {
index 2c35815..7aef4ae 100644 (file)
@@ -35,9 +35,7 @@ class SpecialListUsers extends IncludableSpecialPage {
        }
 
        /**
-        * Show the special page
-        *
-        * @param string $par (optional) A group to list users from
+        * @param string|null $par (optional) A group to list users from
         */
        public function execute( $par ) {
                $this->setHeaders();
index 1f81cf0..1b8ba85 100644 (file)
@@ -39,6 +39,9 @@ class SpecialNewpages extends IncludableSpecialPage {
                parent::__construct( 'Newpages' );
        }
 
+       /**
+        * @param string|null $par
+        */
        protected function setup( $par ) {
                $opts = new FormOptions();
                $this->opts = $opts; // bind
@@ -70,6 +73,9 @@ class SpecialNewpages extends IncludableSpecialPage {
                $opts->validateIntBounds( 'limit', 0, 5000 );
        }
 
+       /**
+        * @param string $par
+        */
        protected function parseParams( $par ) {
                $bits = preg_split( '/\s*,\s*/', trim( $par ) );
                foreach ( $bits as $bit ) {
@@ -115,7 +121,7 @@ class SpecialNewpages extends IncludableSpecialPage {
        /**
         * Show a form for filtering namespace and username
         *
-        * @param string $par
+        * @param string|null $par
         */
        public function execute( $par ) {
                $out = $this->getOutput();
index 46b5520..c8f65c1 100644 (file)
@@ -136,9 +136,7 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
        }
 
        /**
-        * Main execution point
-        *
-        * @param string $subpage
+        * @param string|null $subpage
         */
        public function execute( $subpage ) {
                // Backwards-compatibility: redirect to new feed URLs
index e6d0632..171566b 100644 (file)
@@ -102,7 +102,7 @@ class SpecialSearch extends SpecialPage {
        /**
         * Entry point
         *
-        * @param string $par
+        * @param string|null $par
         */
        public function execute( $par ) {
                $request = $this->getRequest();
@@ -115,7 +115,7 @@ class SpecialSearch extends SpecialPage {
                // parameter, but also as part of the primary url. This can have PII implications
                // in releasing page view data. As such issue a 301 redirect to the correct
                // URL.
-               if ( strlen( $par ) && !strlen( $term ) ) {
+               if ( $par !== null && $par !== '' && $term === '' ) {
                        $query = $request->getValues();
                        unset( $query['title'] );
                        // Strip underscores from title parameter; most of the time we'll want
index dbb1481..5a1b8fb 100644 (file)
@@ -146,8 +146,7 @@ class SpecialUpload extends SpecialPage {
        }
 
        /**
-        * Special page entry point
-        * @param string $par
+        * @param string|null $par
         * @throws ErrorPageError
         * @throws Exception
         * @throws FatalError
index 4d0c20c..fe55d94 100644 (file)
@@ -60,7 +60,7 @@ class SpecialUploadStash extends UnlistedSpecialPage {
        /**
         * Execute page -- can output a file directly or show a listing of them.
         *
-        * @param string $subPage Subpage, e.g. in
+        * @param string|null $subPage Subpage, e.g. in
         *   https://example.com/wiki/Special:UploadStash/foo.jpg, the "foo.jpg" part
         * @return bool Success
         */
index 05bbf3c..8f4d587 100644 (file)
@@ -47,7 +47,6 @@
        "tog-useeditwarning": "Warn me when I leave an edit page with unsaved changes",
        "tog-prefershttps": "Always use a secure connection while logged in",
        "tog-showrollbackconfirmation": "Show a confirmation prompt when clicking on a rollback link",
-       "tog-showrollbackconfirmation-prerelease-warning": "Please note: This feature is not available yet. If you set this preference now, your choice will be remembered [https://meta.wikimedia.org/wiki/WMDE_Technical_Wishes/Rollback#Status when the feature is released].",
        "underline-always": "Always",
        "underline-never": "Never",
        "underline-default": "Skin or browser default",
index 7b0533a..292f682 100644 (file)
        "tog-useeditwarning": "Used as label for the checkbox in [[Special:Preferences#mw-prefsection-editing|Special:Preferences]].",
        "tog-prefershttps": "Toggle option used in [[Special:Preferences]] that indicates if the user wants to use a secure connection when logged in",
        "tog-showrollbackconfirmation": "Toggle option used in [[Special:Preferences]] to enable/disable rollback confirmation prompt. Should be visible only to users with rollback rights.",
-       "tog-showrollbackconfirmation-prerelease-warning": "Notice for wikis where the option can be set before the feature is enabled.\n\nNote: This notice is temporary and will only appear before the rollback confirmation feature is released.",
        "underline-always": "Used in [[Special:Preferences#mw-prefsection-rendering|Preferences]].\n\nThis option means \"always underline links\", there are also options {{msg-mw|Underline-never}} and {{msg-mw|Underline-default}}.\n\n{{Gender}}\n{{Identical|Always}}",
        "underline-never": "Used in [[Special:Preferences#mw-prefsection-rendering|Preferences]].\n\nThis option means \"never underline links\", there are also options {{msg-mw|Underline-always}} and {{msg-mw|Underline-default}}.\n\n{{Gender}}\n{{Identical|Never}}",
        "underline-default": "Used in [[Special:Preferences#mw-prefsection-rendering|Preferences]].\n\nThis option means \"underline links as in your user skin or your browser\", there are also options {{msg-mw|Underline-never}} and {{msg-mw|Underline-always}}.\n\n{{Gender}}\n{{Identical|Browser default}}",
index d000972..dccdd38 100644 (file)
@@ -412,6 +412,7 @@ $filter = $_REQUEST['filter'] ?? '';
        $queries = [];
        $sqltotal = 0.0;
 
+       /** @var profile_point|false $last */
        $last = false;
        foreach ( $res as $o ) {
                $next = new profile_point( $o->pf_name, $o->pf_count, $o->pf_time, $o->pf_memory );
@@ -435,7 +436,7 @@ $filter = $_REQUEST['filter'] ?? '';
                }
        }
 
-       $s = new profile_point( 'SQL Queries', 0, $sqltotal, 0, 0 );
+       $s = new profile_point( 'SQL Queries', 0, $sqltotal, 0 );
        foreach ( $queries as $q ) {
                $s->add_child( $q );
        }
index ec44a83..3f12dc6 100644 (file)
@@ -1507,6 +1507,7 @@ return [
                'scripts' => 'resources/src/mediawiki.action/mediawiki.action.view.rightClickEdit.js',
        ],
        'mediawiki.action.edit.editWarning' => [
+               'targets' => [ 'desktop', 'mobile' ],
                'scripts' => 'resources/src/mediawiki.action/mediawiki.action.edit.editWarning.js',
                'dependencies' => [
                        'jquery.textSelection',