Merge "Remove unused `.success-box` class"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Wed, 18 Sep 2019 20:21:11 +0000 (20:21 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 18 Sep 2019 20:21:11 +0000 (20:21 +0000)
35 files changed:
RELEASE-NOTES-1.34
autoload.php
includes/DevelopmentSettings.php
includes/Rest/BasicAccess/MWBasicAuthorizer.php
includes/Rest/BasicAccess/MWBasicRequestAuthorizer.php
includes/Rest/EntryPoint.php
includes/Rest/Validator/ParamValidatorCallbacks.php
includes/Rest/Validator/Validator.php
includes/api/ApiQueryAllUsers.php
includes/api/ApiQueryBase.php
includes/api/ApiQueryBlockInfoTrait.php [new file with mode: 0644]
includes/api/ApiQueryImageInfo.php
includes/api/ApiQueryUsers.php
includes/filerepo/file/File.php
includes/filerepo/file/LocalFile.php
includes/libs/objectcache/wancache/WANObjectCache.php
includes/objectcache/SqlBagOStuff.php
includes/specials/SpecialListFiles.php
languages/i18n/en.json
languages/i18n/qqq.json
maintenance/Doxyfile
maintenance/mwdocgen.php
resources/Resources.php
resources/src/mediawiki.special/listFiles.less [new file with mode: 0644]
tests/parser/parserTests.txt
tests/phpunit/includes/Rest/BasicAccess/MWBasicRequestAuthorizerTest.php
tests/phpunit/includes/Rest/EntryPointTest.php
tests/phpunit/includes/api/ApiQueryBlockInfoTraitTest.php [new file with mode: 0644]
tests/phpunit/includes/filerepo/file/LocalFileTest.php
tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php
tests/phpunit/unit/includes/Rest/Handler/HelloHandlerTest.php
tests/phpunit/unit/includes/Rest/RouterTest.php
tests/selenium/wdio-mediawiki/CHANGELOG.md
tests/selenium/wdio-mediawiki/README.md
tests/selenium/wdio-mediawiki/package.json

index 09f5346..af87e2b 100644 (file)
@@ -164,6 +164,21 @@ $wgPasswordPolicy['policies']['default']['PasswordNotInLargeBlacklist'] = false;
   deprecated in 1.25, has been removed.
 * (T60993) action=query list=filearchive, list=alldeletedrevisions and
   prop=deletedrevisions no longer require the 'deletedhistory' user right.
+* In the response to queries that use 'prop=imageinfo', entries for
+  non-existing files (indicated by the 'filemissing' field) now omit the
+  following fields, since they are meaningless in this context:
+  'timestamp', 'userhidden', 'user', 'userid', 'anon', 'size', 'width',
+  'height', 'pagecount', 'duration', 'commenthidden', 'parsedcomment',
+  'comment', 'thumburl', 'thumbwidth', 'thumbheight', 'thumbmime',
+  'thumberror', 'url', 'sha1', 'metadata', 'extmetadata', 'commonmetadata',
+  'mime', 'mediadtype', 'bitdepth'.
+  Clients that process these fields should first check if 'filemissing' is
+  set. Fields that are supported even if the file is missing include:
+  'canonicaltitle', ''archivename' (deleted files only), 'descriptionurl',
+  'descriptionshorturl'.
+* The 'blockexpiry' result property in list=users and list=allusers will now be
+  returned in the same format used by the rest of the API: ISO 8601 for
+  expiring blocks, and "infinite" for non-expiring blocks.
 
 === Action API internal changes in 1.34 ===
 * The exception thrown in ApiModuleManager::getModule has been changed
@@ -171,7 +186,7 @@ $wgPasswordPolicy['policies']['default']['PasswordNotInLargeBlacklist'] = false;
   ApiModuleManager::getModule now also throws InvalidArgumentExceptions when
   ObjectFactory is presented with an invalid spec or incorrectly constructed
   objects.
-* 
+* Added ApiQueryBlockInfoTrait.
 
 === Languages updated in 1.34 ===
 MediaWiki supports over 350 languages. Many localisations are updated regularly.
@@ -305,6 +320,8 @@ because of Phabricator reports.
 * The jquery.colorUtil module was removed. Use jquery.color instead.
 * The jquery.checkboxShiftClick module was removed. The functionality
   is provided by mediawiki.page.ready instead (T232688).
+* The 'jquery.accessKeyLabel' module has been removed. This jQuery
+  plugin now ships as part of the 'mediawiki.util' module bundle.
 * EditPage::submit(), deprecated in 1.29, has been removed. Use $this->edit()
   directly.
 * HTMLForm::getErrors(), deprecated in 1.28, has been removed. Use
@@ -489,8 +506,6 @@ because of Phabricator reports.
 * ResourceLoaderContext::getConfig and ResourceLoaderContext::getLogger have
   been deprecated. Inside ResourceLoaderModule subclasses, use the local methods
   instead. Elsewhere, use the methods from the ResourceLoader class.
-* The 'jquery.accessKeyLabel' module has been deprecated. This jQuery
-  plugin is now ships as part of the 'mediawiki.util' module bundle.
 * The Profiler::setTemplated and Profiler::getTemplated methods have been
   deprecated. Use Profiler::setAllowOutput and Profiler::getAllowOutput
   instead.
@@ -575,6 +590,8 @@ because of Phabricator reports.
 * Global variable $wgSysopEmailBans is deprecated; to allow sysops to ban
   users from sending emails, use
   $wgGroupPermissions['sysop']['blockemail'] = true;
+* ApiQueryBase::showHiddenUsersAddBlockInfo() is deprecated. Use
+  ApiQueryBlockInfoTrait instead.
 
 === Other changes in 1.34 ===
 * …
index 48d5b30..7ff29ce 100644 (file)
@@ -89,6 +89,7 @@ $wgAutoloadLocalClasses = [
        'ApiQueryBacklinks' => __DIR__ . '/includes/api/ApiQueryBacklinks.php',
        'ApiQueryBacklinksprop' => __DIR__ . '/includes/api/ApiQueryBacklinksprop.php',
        'ApiQueryBase' => __DIR__ . '/includes/api/ApiQueryBase.php',
+       'ApiQueryBlockInfoTrait' => __DIR__ . '/includes/api/ApiQueryBlockInfoTrait.php',
        'ApiQueryBlocks' => __DIR__ . '/includes/api/ApiQueryBlocks.php',
        'ApiQueryCategories' => __DIR__ . '/includes/api/ApiQueryCategories.php',
        'ApiQueryCategoryInfo' => __DIR__ . '/includes/api/ApiQueryCategoryInfo.php',
index d93caa7..668de39 100644 (file)
@@ -27,7 +27,7 @@ ini_set( 'display_errors', 1 );
 
 global $wgDevelopmentWarnings, $wgShowExceptionDetails, $wgShowHostnames,
        $wgDebugRawPage, $wgCommandLineMode, $wgDebugLogFile,
-       $wgDBerrorLog, $wgDebugLogGroups;
+       $wgDBerrorLog, $wgDebugLogGroups, $wgLocalisationCacheConf;
 
 // Use of wfWarn() should cause tests to fail
 $wgDevelopmentWarnings = true;
@@ -74,3 +74,6 @@ $wgSQLMode = 'TRADITIONAL';
 
 // Disable legacy javascript globals in CI and for devs (T72470)
 $wgLegacyJavaScriptGlobals = false;
+
+// Localisation Cache to StaticArray (T218207)
+$wgLocalisationCacheConf['store'] = 'array';
index 43014f1..92529b3 100644 (file)
@@ -2,24 +2,24 @@
 
 namespace MediaWiki\Rest\BasicAccess;
 
-use User;
 use MediaWiki\Permissions\PermissionManager;
 use MediaWiki\Rest\Handler;
 use MediaWiki\Rest\RequestInterface;
+use MediaWiki\User\UserIdentity;
 
 /**
- * A factory for MWBasicRequestAuthorizer which passes through a User object
+ * A factory for MWBasicRequestAuthorizer which passes through a UserIdentity.
  *
  * @internal
  */
 class MWBasicAuthorizer extends BasicAuthorizerBase {
-       /** @var User */
+       /** @var UserIdentity */
        private $user;
 
        /** @var PermissionManager */
        private $permissionManager;
 
-       public function __construct( User $user, PermissionManager $permissionManager ) {
+       public function __construct( UserIdentity $user, PermissionManager $permissionManager ) {
                $this->user = $user;
                $this->permissionManager = $permissionManager;
        }
index 8c459c6..671488a 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace MediaWiki\Rest\BasicAccess;
 
-use User;
+use MediaWiki\User\UserIdentity;
 use MediaWiki\Permissions\PermissionManager;
 use MediaWiki\Rest\Handler;
 use MediaWiki\Rest\RequestInterface;
@@ -13,14 +13,14 @@ use MediaWiki\Rest\RequestInterface;
  * @internal
  */
 class MWBasicRequestAuthorizer extends BasicRequestAuthorizer {
-       /** @var User */
+       /** @var UserIdentity */
        private $user;
 
        /** @var PermissionManager */
        private $permissionManager;
 
        public function __construct( RequestInterface $request, Handler $handler,
-               User $user, PermissionManager $permissionManager
+               UserIdentity $user, PermissionManager $permissionManager
        ) {
                parent::__construct( $request, $handler );
                $this->user = $user;
index 4fdd1f8..070451d 100644 (file)
@@ -57,7 +57,11 @@ class EntryPoint {
                        $services->getPermissionManager() );
 
                // @phan-suppress-next-line PhanAccessMethodInternal
-               $restValidator = new Validator( $objectFactory, $request, RequestContext::getMain()->getUser() );
+               $restValidator = new Validator( $objectFactory,
+                       $services->getPermissionManager(),
+                       $request,
+                       RequestContext::getMain()->getUser()
+               );
 
                global $IP;
                $router = new Router(
index 6c54a50..93de911 100644 (file)
@@ -3,21 +3,30 @@
 namespace MediaWiki\Rest\Validator;
 
 use InvalidArgumentException;
+use MediaWiki\Permissions\PermissionManager;
 use MediaWiki\Rest\RequestInterface;
+use MediaWiki\User\UserIdentity;
 use Psr\Http\Message\UploadedFileInterface;
-use User;
 use Wikimedia\ParamValidator\Callbacks;
 use Wikimedia\ParamValidator\ValidationException;
 
 class ParamValidatorCallbacks implements Callbacks {
 
+       /** @var PermissionManager */
+       private $permissionManager;
+
        /** @var RequestInterface */
        private $request;
 
-       /** @var User */
+       /** @var UserIdentity */
        private $user;
 
-       public function __construct( RequestInterface $request, User $user ) {
+       public function __construct(
+               PermissionManager $permissionManager,
+               RequestInterface $request,
+               UserIdentity $user
+       ) {
+               $this->permissionManager = $permissionManager;
                $this->request = $request;
                $this->user = $user;
        }
@@ -76,7 +85,7 @@ class ParamValidatorCallbacks implements Callbacks {
        }
 
        public function useHighLimits( array $options ) {
-               return $this->user->isAllowed( 'apihighlimits' );
+               return $this->permissionManager->userHasRight( $this->user, 'apihighlimits' );
        }
 
 }
index cee1cdb..be8d7a4 100644 (file)
@@ -2,10 +2,11 @@
 
 namespace MediaWiki\Rest\Validator;
 
+use MediaWiki\Permissions\PermissionManager;
 use MediaWiki\Rest\Handler;
 use MediaWiki\Rest\HttpException;
 use MediaWiki\Rest\RequestInterface;
-use User;
+use MediaWiki\User\UserIdentity;
 use Wikimedia\ObjectFactory;
 use Wikimedia\ParamValidator\ParamValidator;
 use Wikimedia\ParamValidator\TypeDef\BooleanDef;
@@ -62,16 +63,20 @@ class Validator {
        private $paramValidator;
 
        /**
-        * @internal
         * @param ObjectFactory $objectFactory
+        * @param PermissionManager $permissionManager
         * @param RequestInterface $request
-        * @param User $user
+        * @param UserIdentity $user
+        * @internal
         */
        public function __construct(
-               ObjectFactory $objectFactory, RequestInterface $request, User $user
+               ObjectFactory $objectFactory,
+               PermissionManager $permissionManager,
+               RequestInterface $request,
+               UserIdentity $user
        ) {
                $this->paramValidator = new ParamValidator(
-                       new ParamValidatorCallbacks( $request, $user ),
+                       new ParamValidatorCallbacks( $permissionManager, $request, $user ),
                        $objectFactory,
                        [
                                'typeDefs' => self::$typeDefs,
index a7d4fb9..0ea6af3 100644 (file)
  * @file
  */
 
+use MediaWiki\Block\DatabaseBlock;
+
 /**
  * Query module to enumerate all registered users.
  *
  * @ingroup API
  */
 class ApiQueryAllUsers extends ApiQueryBase {
+       use ApiQueryBlockInfoTrait;
+
        public function __construct( ApiQuery $query, $moduleName ) {
                parent::__construct( $query, $moduleName, 'au' );
        }
@@ -153,7 +157,7 @@ class ApiQueryAllUsers extends ApiQueryBase {
                        $this->addWhere( 'user_editcount > 0' );
                }
 
-               $this->showHiddenUsersAddBlockInfo( $fld_blockinfo );
+               $this->addBlockInfoToQuery( $fld_blockinfo );
 
                if ( $fld_groups || $fld_rights ) {
                        $this->addFields( [ 'groups' =>
@@ -263,13 +267,8 @@ class ApiQueryAllUsers extends ApiQueryBase {
                                );
                        }
 
-                       if ( $fld_blockinfo && !is_null( $row->ipb_by_text ) ) {
-                               $data['blockid'] = (int)$row->ipb_id;
-                               $data['blockedby'] = $row->ipb_by_text;
-                               $data['blockedbyid'] = (int)$row->ipb_by;
-                               $data['blockedtimestamp'] = wfTimestamp( TS_ISO_8601, $row->ipb_timestamp );
-                               $data['blockreason'] = $commentStore->getComment( 'ipb_reason', $row )->text;
-                               $data['blockexpiry'] = $row->ipb_expiry;
+                       if ( $fld_blockinfo && !is_null( $row->ipb_id ) ) {
+                               $data += $this->getBlockDetails( DatabaseBlock::newFromRow( $row ) );
                        }
                        if ( $row->ipb_deleted ) {
                                $data['hidden'] = true;
index 10db848..8d9cb48 100644 (file)
@@ -31,6 +31,7 @@ use Wikimedia\Rdbms\IResultWrapper;
  * @ingroup API
  */
 abstract class ApiQueryBase extends ApiBase {
+       use ApiQueryBlockInfoTrait;
 
        private $mQueryModule, $mDb, $tables, $where, $fields, $options, $join_conds;
 
@@ -424,47 +425,6 @@ abstract class ApiQueryBase extends ApiBase {
                return Hooks::run( 'ApiQueryBaseProcessRow', [ $this, $row, &$data, &$hookData ] );
        }
 
-       /**
-        * Filters hidden users (where the user doesn't have the right to view them)
-        * Also adds relevant block information
-        *
-        * @param bool $showBlockInfo
-        * @return void
-        */
-       public function showHiddenUsersAddBlockInfo( $showBlockInfo ) {
-               $db = $this->getDB();
-
-               $tables = [ 'ipblocks' ];
-               $fields = [ 'ipb_deleted' ];
-               $joinConds = [
-                       'blk' => [ 'LEFT JOIN', [
-                               'ipb_user=user_id',
-                               'ipb_expiry > ' . $db->addQuotes( $db->timestamp() ),
-                       ] ],
-               ];
-
-               if ( $showBlockInfo ) {
-                       $actorQuery = ActorMigration::newMigration()->getJoin( 'ipb_by' );
-                       $commentQuery = CommentStore::getStore()->getJoin( 'ipb_reason' );
-                       $tables += $actorQuery['tables'] + $commentQuery['tables'];
-                       $joinConds += $actorQuery['joins'] + $commentQuery['joins'];
-                       $fields = array_merge( $fields, [
-                               'ipb_id',
-                               'ipb_expiry',
-                               'ipb_timestamp'
-                       ], $actorQuery['fields'], $commentQuery['fields'] );
-               }
-
-               $this->addTables( [ 'blk' => $tables ] );
-               $this->addFields( $fields );
-               $this->addJoinConds( $joinConds );
-
-               // Don't show hidden names
-               if ( !$this->getPermissionManager()->userHasRight( $this->getUser(), 'hideuser' ) ) {
-                       $this->addWhere( 'ipb_deleted = 0 OR ipb_deleted IS NULL' );
-               }
-       }
-
        /** @} */
 
        /************************************************************************//**
@@ -610,4 +570,24 @@ abstract class ApiQueryBase extends ApiBase {
        }
 
        /** @} */
+
+       /************************************************************************//**
+        * @name   Deprecated methods
+        * @{
+        */
+
+       /**
+        * Filters hidden users (where the user doesn't have the right to view them)
+        * Also adds relevant block information
+        *
+        * @deprecated since 1.34, use ApiQueryBlockInfoTrait instead
+        * @param bool $showBlockInfo
+        * @return void
+        */
+       public function showHiddenUsersAddBlockInfo( $showBlockInfo ) {
+               wfDeprecated( __METHOD__, '1.34' );
+               return $this->addBlockInfoToQuery( $showBlockInfo );
+       }
+
+       /** @} */
 }
diff --git a/includes/api/ApiQueryBlockInfoTrait.php b/includes/api/ApiQueryBlockInfoTrait.php
new file mode 100644 (file)
index 0000000..a3be356
--- /dev/null
@@ -0,0 +1,94 @@
+<?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
+ */
+
+use MediaWiki\Block\DatabaseBlock;
+use MediaWiki\Permissions\PermissionManager;
+
+/**
+ * @ingroup API
+ */
+trait ApiQueryBlockInfoTrait {
+       use ApiBlockInfoTrait;
+
+       /**
+        * Filters hidden users (where the user doesn't have the right to view them)
+        * Also adds relevant block information
+        *
+        * @param bool $showBlockInfo
+        * @return void
+        */
+       private function addBlockInfoToQuery( $showBlockInfo ) {
+               $db = $this->getDB();
+
+               if ( $showBlockInfo ) {
+                       $queryInfo = DatabaseBlock::getQueryInfo();
+               } else {
+                       $queryInfo = [
+                               'tables' => [ 'ipblocks' ],
+                               'fields' => [ 'ipb_deleted' ],
+                               'joins' => [],
+                       ];
+               }
+
+               $this->addTables( [ 'blk' => $queryInfo['tables'] ] );
+               $this->addFields( $queryInfo['fields'] );
+               $this->addJoinConds( $queryInfo['joins'] );
+               $this->addJoinConds( [
+                       'blk' => [ 'LEFT JOIN', [
+                               'ipb_user=user_id',
+                               'ipb_expiry > ' . $db->addQuotes( $db->timestamp() ),
+                       ] ],
+               ] );
+
+               // Don't show hidden names
+               if ( !$this->getPermissionManager()->userHasRight( $this->getUser(), 'hideuser' ) ) {
+                       $this->addWhere( 'ipb_deleted = 0 OR ipb_deleted IS NULL' );
+               }
+       }
+
+       /**
+        * @name Methods required from ApiQueryBase
+        * @{
+        */
+
+       /** @see ApiBase::getDB */
+       abstract protected function getDB();
+
+       /** @see ApiBase::getPermissionManager */
+       abstract protected function getPermissionManager(): PermissionManager;
+
+       /** @see IContextSource::getUser */
+       abstract public function getUser();
+
+       /** @see ApiQueryBase::addTables */
+       abstract protected function addTables( $tables, $alias = null );
+
+       /** @see ApiQueryBase::addFields */
+       abstract protected function addFields( $fields );
+
+       /** @see ApiQueryBase::addWhere */
+       abstract protected function addWhere( $conds );
+
+       /** @see ApiQueryBase::addJoinConds */
+       abstract protected function addJoinConds( $conds );
+
+       /**@}*/
+
+}
index 97a9b0a..285c0bf 100644 (file)
@@ -387,9 +387,13 @@ class ApiQueryImageInfo extends ApiQueryBase {
                $vals = [
                        ApiResult::META_TYPE => 'assoc',
                ];
+
+               // Some information will be unavailable if the file does not exist. T221812
+               $exists = $file->exists();
+
                // Timestamp is shown even if the file is revdelete'd in interface
                // so do same here.
-               if ( isset( $prop['timestamp'] ) ) {
+               if ( isset( $prop['timestamp'] ) && $exists ) {
                        $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $file->getTimestamp() );
                }
 
@@ -408,7 +412,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
                $user = isset( $prop['user'] );
                $userid = isset( $prop['userid'] );
 
-               if ( $user || $userid ) {
+               if ( ( $user || $userid ) && $exists ) {
                        if ( $file->isDeleted( File::DELETED_USER ) ) {
                                $vals['userhidden'] = true;
                                $anyHidden = true;
@@ -428,7 +432,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
 
                // This is shown even if the file is revdelete'd in interface
                // so do same here.
-               if ( isset( $prop['size'] ) || isset( $prop['dimensions'] ) ) {
+               if ( ( isset( $prop['size'] ) || isset( $prop['dimensions'] ) ) && $exists ) {
                        $vals['size'] = (int)$file->getSize();
                        $vals['width'] = (int)$file->getWidth();
                        $vals['height'] = (int)$file->getHeight();
@@ -449,7 +453,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
                $pcomment = isset( $prop['parsedcomment'] );
                $comment = isset( $prop['comment'] );
 
-               if ( $pcomment || $comment ) {
+               if ( ( $pcomment || $comment ) && $exists ) {
                        if ( $file->isDeleted( File::DELETED_COMMENT ) ) {
                                $vals['commenthidden'] = true;
                                $anyHidden = true;
@@ -500,7 +504,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
                }
 
                if ( $url ) {
-                       if ( $file->exists() ) {
+                       if ( $exists ) {
                                if ( !is_null( $thumbParams ) ) {
                                        $mto = $file->transform( $thumbParams );
                                        self::$transformCount++;
@@ -529,8 +533,6 @@ class ApiQueryImageInfo extends ApiQueryBase {
                                        }
                                }
                                $vals['url'] = wfExpandUrl( $file->getFullUrl(), PROTO_CURRENT );
-                       } else {
-                               $vals['filemissing'] = true;
                        }
                        $vals['descriptionurl'] = wfExpandUrl( $file->getDescriptionUrl(), PROTO_CURRENT );
 
@@ -540,11 +542,15 @@ class ApiQueryImageInfo extends ApiQueryBase {
                        }
                }
 
-               if ( $sha1 ) {
+               if ( !$exists ) {
+                       $vals['filemissing'] = true;
+               }
+
+               if ( $sha1 && $exists ) {
                        $vals['sha1'] = Wikimedia\base_convert( $file->getSha1(), 36, 16, 40 );
                }
 
-               if ( $meta ) {
+               if ( $meta && $exists ) {
                        Wikimedia\suppressWarnings();
                        $metadata = unserialize( $file->getMetadata() );
                        Wikimedia\restoreWarnings();
@@ -553,12 +559,12 @@ class ApiQueryImageInfo extends ApiQueryBase {
                        }
                        $vals['metadata'] = $metadata ? static::processMetaData( $metadata, $result ) : null;
                }
-               if ( $commonmeta ) {
+               if ( $commonmeta && $exists ) {
                        $metaArray = $file->getCommonMetaArray();
                        $vals['commonmetadata'] = $metaArray ? static::processMetaData( $metaArray, $result ) : [];
                }
 
-               if ( $extmetadata ) {
+               if ( $extmetadata && $exists ) {
                        // Note, this should return an array where all the keys
                        // start with a letter, and all the values are strings.
                        // Thus there should be no issue with format=xml.
@@ -575,11 +581,11 @@ class ApiQueryImageInfo extends ApiQueryBase {
                        $vals['extmetadata'] = $extmetaArray;
                }
 
-               if ( $mime ) {
+               if ( $mime && $exists ) {
                        $vals['mime'] = $file->getMimeType();
                }
 
-               if ( $mediatype ) {
+               if ( $mediatype && $exists ) {
                        $vals['mediatype'] = $file->getMediaType();
                }
 
@@ -589,7 +595,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
                        $vals['archivename'] = $file->getArchiveName();
                }
 
-               if ( $bitdepth ) {
+               if ( $bitdepth && $exists ) {
                        $vals['bitdepth'] = $file->getBitDepth();
                }
 
index ce51a67..0171a37 100644 (file)
  * @file
  */
 
+use MediaWiki\Block\DatabaseBlock;
+
 /**
  * Query module to get information about a list of users
  *
  * @ingroup API
  */
 class ApiQueryUsers extends ApiQueryBase {
+       use ApiQueryBlockInfoTrait;
 
        private $tokenFunctions, $prop;
 
@@ -150,7 +153,7 @@ class ApiQueryUsers extends ApiQueryBase {
                                $this->addWhereFld( 'user_id', $userids );
                        }
 
-                       $this->showHiddenUsersAddBlockInfo( isset( $this->prop['blockinfo'] ) );
+                       $this->addBlockInfoToQuery( isset( $this->prop['blockinfo'] ) );
 
                        $data = [];
                        $res = $this->select( __METHOD__ );
@@ -232,13 +235,7 @@ class ApiQueryUsers extends ApiQueryBase {
                                        $data[$key]['hidden'] = true;
                                }
                                if ( isset( $this->prop['blockinfo'] ) && !is_null( $row->ipb_by_text ) ) {
-                                       $data[$key]['blockid'] = (int)$row->ipb_id;
-                                       $data[$key]['blockedby'] = $row->ipb_by_text;
-                                       $data[$key]['blockedbyid'] = (int)$row->ipb_by;
-                                       $data[$key]['blockedtimestamp'] = wfTimestamp( TS_ISO_8601, $row->ipb_timestamp );
-                                       $data[$key]['blockreason'] = $commentStore->getComment( 'ipb_reason', $row )
-                                               ->text;
-                                       $data[$key]['blockexpiry'] = $row->ipb_expiry;
+                                       $data[$key] += $this->getBlockDetails( DatabaseBlock::newFromRow( $row ) );
                                }
 
                                if ( isset( $this->prop['emailable'] ) ) {
index 60d4e29..73b08e6 100644 (file)
@@ -2042,7 +2042,7 @@ abstract class File implements IDBAccessObject {
         * Get the URL of the image description page. May return false if it is
         * unknown or not applicable.
         *
-        * @return string
+        * @return string|bool
         */
        function getDescriptionUrl() {
                if ( $this->repo ) {
index ceb8dda..6d29433 100644 (file)
@@ -452,6 +452,10 @@ class LocalFile extends File {
         * This covers fields that are sometimes not cached.
         */
        protected function loadExtraFromDB() {
+               if ( !$this->title ) {
+                       return; // Avoid hard failure when the file does not exist. T221812
+               }
+
                $fname = static::class . '::' . __FUNCTION__;
 
                # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
@@ -857,12 +861,24 @@ class LocalFile extends File {
        function getUser( $type = 'text' ) {
                $this->load();
 
-               if ( $type === 'object' ) {
-                       return $this->user;
-               } elseif ( $type === 'text' ) {
-                       return $this->user->getName();
-               } elseif ( $type === 'id' ) {
-                       return $this->user->getId();
+               if ( !$this->user ) {
+                       // If the file does not exist, $this->user will be null, see T221812.
+                       // Note: 'Unknown user' this is a reserved user name.
+                       if ( $type === 'object' ) {
+                               return User::newFromName( 'Unknown user', false );
+                       } elseif ( $type === 'text' ) {
+                               return 'Unknown user';
+                       } elseif ( $type === 'id' ) {
+                               return 0;
+                       }
+               } else {
+                       if ( $type === 'object' ) {
+                               return $this->user;
+                       } elseif ( $type === 'text' ) {
+                               return $this->user->getName();
+                       } elseif ( $type === 'id' ) {
+                               return $this->user->getId();
+                       }
                }
 
                throw new MWException( "Unknown type '$type'." );
@@ -876,9 +892,13 @@ class LocalFile extends File {
         * @since 1.27
         */
        public function getDescriptionShortUrl() {
+               if ( !$this->title ) {
+                       return null; // Avoid hard failure when the file does not exist. T221812
+               }
+
                $pageId = $this->title->getArticleID();
 
-               if ( $pageId !== null ) {
+               if ( $pageId ) {
                        $url = $this->repo->makeUrl( [ 'curid' => $pageId ] );
                        if ( $url !== false ) {
                                return $url;
@@ -1145,6 +1165,10 @@ class LocalFile extends File {
         * @return OldLocalFile[]
         */
        function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
+               if ( !$this->exists() ) {
+                       return []; // Avoid hard failure when the file does not exist. T221812
+               }
+
                $dbr = $this->repo->getReplicaDB();
                $oldFileQuery = OldLocalFile::getQueryInfo();
 
@@ -1198,9 +1222,13 @@ class LocalFile extends File {
         *  0      return line for current version
         *  1      query for old versions, return first one
         *  2, ... return next old version from above query
-        * @return bool
+        * @return stdClass|bool
         */
        public function nextHistoryLine() {
+               if ( !$this->exists() ) {
+                       return false; // Avoid hard failure when the file does not exist. T221812
+               }
+
                # Polymorphic function name to distinguish foreign and local fetches
                $fname = static::class . '::' . __FUNCTION__;
 
@@ -2026,9 +2054,13 @@ class LocalFile extends File {
 
        /**
         * Get the URL of the file description page.
-        * @return string
+        * @return string|bool
         */
        function getDescriptionUrl() {
+               if ( !$this->title ) {
+                       return false; // Avoid hard failure when the file does not exist. T221812
+               }
+
                return $this->title->getLocalURL();
        }
 
@@ -2041,6 +2073,10 @@ class LocalFile extends File {
         * @return string|false
         */
        function getDescriptionText( Language $lang = null ) {
+               if ( !$this->title ) {
+                       return false; // Avoid hard failure when the file does not exist. T221812
+               }
+
                $store = MediaWikiServices::getInstance()->getRevisionStore();
                $revision = $store->getRevisionByTitle( $this->title, 0, Revision::READ_NORMAL );
                if ( !$revision ) {
@@ -2090,6 +2126,10 @@ class LocalFile extends File {
         * @return bool|string
         */
        public function getDescriptionTouched() {
+               if ( !$this->exists() ) {
+                       return false; // Avoid hard failure when the file does not exist. T221812
+               }
+
                // The DB lookup might return false, e.g. if the file was just deleted, or the shared DB repo
                // itself gets it from elsewhere. To avoid repeating the DB lookups in such a case, we
                // need to differentiate between null (uninitialized) and false (failed to load).
index 629d2cd..70f3553 100644 (file)
@@ -2488,8 +2488,9 @@ class WANObjectCache implements IExpiringStore, IStoreKeyEncoder, LoggerAwareInt
         */
        private function determineKeyClassForStats( $key ) {
                $parts = explode( ':', $key, 3 );
-
-               return $parts[1] ?? $parts[0]; // sanity
+               // Sanity fallback in case the key was not made by makeKey.
+               // Replace dots because they are special in StatsD (T232907)
+               return strtr( $parts[1] ?? $parts[0], '.', '_' );
        }
 
        /**
index d713396..4a3445e 100644 (file)
@@ -194,6 +194,10 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                                $conn = Database::factory( $type, $info );
                                $conn->clearFlag( DBO_TRX ); // auto-commit mode
                                $this->conns[$shardIndex] = $conn;
+                               // Automatically create the objectcache table for sqlite as needed
+                               if ( $conn->getType() === 'sqlite' ) {
+                                       $this->initSqliteDatabase( $conn );
+                               }
                        }
                        $conn = $this->conns[$shardIndex];
                } else {
@@ -206,10 +210,6 @@ class SqlBagOStuff extends MediumSpecificBagOStuff {
                        $attribs = $lb->getServerAttributes( $lb->getWriterIndex() );
                        $flags = $attribs[Database::ATTR_DB_LEVEL_LOCKING] ? 0 : $lb::CONN_TRX_AUTOCOMMIT;
                        $conn = $lb->getMaintenanceConnectionRef( $index, [], false, $flags );
-                       // Automatically create the objectcache table for sqlite as needed
-                       if ( $conn->getType() === 'sqlite' ) {
-                               $this->initSqliteDatabase( $conn );
-                       }
                }
 
                $this->logger->debug( sprintf( "Connection %s will be used for SqlBagOStuff", $conn ) );
index 02a468b..bc4f7e2 100644 (file)
@@ -40,6 +40,11 @@ class SpecialListFiles extends IncludableSpecialPage {
                        $search = $this->getRequest()->getText( 'ilsearch', '' );
                        $showAll = $this->getRequest()->getBool( 'ilshowall', false );
                }
+               if ( $userName ) {
+                       $pageTitle = $this->msg( 'listfiles_subpage', $userName );
+               } else {
+                       $pageTitle = $this->msg( 'listfiles' );
+               }
 
                $pager = new ImageListPager(
                        $this->getContext(),
@@ -51,6 +56,8 @@ class SpecialListFiles extends IncludableSpecialPage {
                );
 
                $out = $this->getOutput();
+               $out->setPageTitle( $pageTitle );
+               $out->addModuleStyles( 'mediawiki.special' );
                if ( $this->including() ) {
                        $out->addParserOutputContent( $pager->getBodyOutput() );
                } else {
index 69d4fe2..7944a37 100644 (file)
        "resettokens-watchlist-token": "Token for the web feed (Atom/RSS) of [[Special:Watchlist|changes to pages on your watchlist]]",
        "resettokens-done": "Tokens reset.",
        "resettokens-resetbutton": "Reset selected tokens",
-       "bold_sample": "Bold text",
-       "bold_tip": "Bold text",
-       "italic_sample": "Italic text",
-       "italic_tip": "Italic text",
-       "link_sample": "Link title",
-       "link_tip": "Internal link",
-       "extlink_sample": "http://www.example.com link title",
-       "extlink_tip": "External link (remember http:// prefix)",
-       "headline_sample": "Headline text",
-       "headline_tip": "Level 2 headline",
-       "nowiki_sample": "Insert non-formatted text here",
-       "nowiki_tip": "Ignore wiki formatting",
-       "image_sample": "Example.jpg",
-       "image_tip": "Embedded file",
-       "media_sample": "Example.ogg",
-       "media_tip": "File link",
        "sig-text": "--$1",
-       "sig_tip": "Your signature with timestamp",
-       "hr_tip": "Horizontal line (use sparingly)",
        "summary": "Summary:",
        "subject": "Subject:",
        "minoredit": "This is a minor edit",
        "listfiles-userdoesnotexist": "User account \"$1\" is not registered.",
        "imgfile": "file",
        "listfiles": "File list",
+       "listfiles_subpage": "Uploads by $1",
        "listfiles_thumb": "Thumbnail",
        "listfiles_date": "Date",
        "listfiles_name": "Name",
index 436c11b..dc8c3a7 100644 (file)
        "resettokens-watchlist-token": "Label for watchlist token checkbox on [[Special:ResetTokens]] (see {{msg-mw|prefs-watchlist-token}} at [[Special:Preferences#mw-prefsection-watchlist]]).",
        "resettokens-done": "Message shown on [[Special:ResetTokens]] after the tokens have been reset successfully.",
        "resettokens-resetbutton": "Form submit button on [[Special:ResetTokens]].",
-       "bold_sample": "This is the sample text that you get when you press the first button on the left on the edit toolbar.\n\n{{Identical|Bold text}}",
-       "bold_tip": "This is the text that appears when you hover the mouse over the first button on the left of the edit toolbar.\n\n{{Identical|Bold text}}",
-       "italic_sample": "The sample text that you get when you press the second button from the left on the edit toolbar.\n\n{{Identical|Italic text}}",
-       "italic_tip": "This is the tooltip that appears when the user points to the \"Italic\" button in the edit toolbar.\n\n{{Identical|Italic text}}",
-       "link_sample": "This is the default text in the internal link that is created when you press the third button from the left on the edit toolbar (the \"Ab\" icon).",
-       "link_tip": "Tip for internal links.\n{{Identical|Internal link}}",
-       "extlink_sample": "This message appears when clicking on the fourth button of the edit toolbar. You can translate \"link title\". Because many of the localisations had urls that went to domains reserved for advertising, it is recommended that the link is left as-is. All customised links were replaced with the standard one, that is reserved in the standard and will never have ads or something.",
-       "extlink_tip": "This is the tip that appears when you hover the mouse over the fourth button from the left on the edit toolbar.\n\n{{Identical|External link (remember http:// prefix)}}",
-       "headline_sample": "Sample of headline text.",
-       "headline_tip": "This is the text that appears when you hover the mouse over the fifth button from the left on the edit toolbar.",
-       "nowiki_sample": "Text inserted between nowiki tags",
-       "nowiki_tip": "This is the text that appears when you hover the mouse over the third button from the right on the edit toolbar.",
-       "image_sample": "{{optional}}\nUsed in text generated by Picture button in toolbar.\n{{Identical|Example}}",
-       "image_tip": "This is the text that appears when you hover the mouse over the sixth (middle) button on the edit toolbar.\n\n{{Identical|Embedded file}}",
-       "media_sample": "{{optional}}\n{{Identical|Example}}",
-       "media_tip": "This is the text that appears when you hover the mouse over the fifth button from the right in the edit toolbar.\n{{Identical|File link}}",
        "sig-text": "{{notranslate}} This is the text that appears when you click on the signature button (second button from the right) on the edit toolbar. $1 will be replaced with four tildes (which cannot be included directly in the message for technical reasons).",
-       "sig_tip": "This is the text that appears when you hover the mouse over the second key from the right on the edit toolbar.\n{{Identical|Signature with timestamp}}",
-       "hr_tip": "This is the text that appears when you hover the mouse over the first button on the right on the edit toolbar.",
        "summary": "The Summary text beside the edit summary field\n\nSee also:\n* {{msg-mw|Subject}}\nSee also:\n* {{msg-mw|Accesskey-summary}}\n* {{msg-mw|Tooltip-summary}}\n{{Identical|Summary}}",
        "subject": "Used as label for the section title input box when adding a new section on a talk page.\n\nSee also:\n* {{msg-mw|Summary}}\n{{Identical|Subject}}",
        "minoredit": "Text above Save page button in editor\n\nSee also:\n* {{msg-mw|Minoredit}}\n* {{msg-mw|Accesskey-minoredit}}\n* {{msg-mw|Tooltip-minoredit}}",
        "listfiles-userdoesnotexist": "This message is displayed on [[Special:ListFiles]] when a invalid username is entered.",
        "imgfile": "{{Identical|File}}",
        "listfiles": "Page title and grouping label for the form displayed on [[Special:ListFiles]].\n{{Identical|List}}",
+       "listfiles_subpage": "Page title and grouping label for the form displayed on [[Special:ListFiles]].\n{{Identical|List}} when a username is selected. Parameters:\n * $1 - username",
        "listfiles_thumb": "{{Identical|Thumbnail}}",
        "listfiles_date": "Column header for the result table displayed on [[Special:ListFiles]].\n{{Identical|Date}}",
        "listfiles_name": "Column header for the result table displayed on [[Special:ListFiles]].\n{{Identical|Name}}",
index 6ea046e..0d1579d 100644 (file)
@@ -143,6 +143,7 @@ EXCLUDE_PATTERNS       = LocalSettings.php \
                          AdminSettings.php \
                          .svn \
                          */.git/* \
+                         */README.md \
                          {{EXCLUDE_PATTERNS}}
 EXCLUDE_SYMBOLS        =
 EXAMPLE_PATH           =
index 4401ea8..6b22097 100644 (file)
@@ -83,8 +83,10 @@ class MWDocGen extends Maintenance {
                $this->addOption( 'output',
                        'Path to write doc to',
                        false, true );
-               $this->addOption( 'no-extensions',
-                       'Ignore extensions' );
+               $this->addOption( 'extensions',
+                       'Process the extensions/ directory as well (ignored if --file is used)' );
+               $this->addOption( 'skins',
+                       'Process the skins/ directory as well (ignored if --file is used)' );
        }
 
        public function getDbType() {
@@ -115,22 +117,23 @@ class MWDocGen extends Maintenance {
 
                $this->template = $IP . '/maintenance/Doxyfile';
                $this->excludes = [
-                       'vendor',
-                       'node_modules',
-                       'resources/lib',
                        'images',
+                       'node_modules',
+                       'resources',
                        'static',
                        'tests',
-                       'includes/libs/Message/README.md',
-                       'includes/libs/objectcache/README.md',
-                       'includes/libs/ParamValidator/README.md',
-                       'maintenance/benchmarks/README.md',
-                       'resources/src/mediawiki.ui/styleguide.md',
+                       'vendor',
                ];
                $this->excludePatterns = [];
-               if ( $this->hasOption( 'no-extensions' ) ) {
-                       $this->excludePatterns[] = 'extensions';
-                       $this->excludePatterns[] = 'skins';
+               if ( $this->input === '' ) {
+                       // If no explicit --file filter is set, we're indexing all of $IP,
+                       // but any extension or skin submodules should be excluded by default.
+                       if ( !$this->hasOption( 'extensions' ) ) {
+                               $this->excludePatterns[] = 'extensions';
+                       }
+                       if ( !$this->hasOption( 'skins' ) ) {
+                               $this->excludePatterns[] = 'skins';
+                       }
                }
 
                $this->doDot = shell_exec( 'which dot' );
index c414c7b..c8eae03 100644 (file)
@@ -155,13 +155,6 @@ return [
 
        /* jQuery Plugins */
 
-       'jquery.accessKeyLabel' => [
-               'deprecated' => 'Please use "mediawiki.util" instead.',
-               'dependencies' => [
-                       'mediawiki.util',
-               ],
-               'targets' => [ 'mobile', 'desktop' ],
-       ],
        'jquery.chosen' => [
                'scripts' => 'resources/lib/jquery.chosen/chosen.jquery.js',
                'styles' => 'resources/lib/jquery.chosen/chosen.css',
@@ -1998,6 +1991,7 @@ return [
                        'resources/src/mediawiki.special/userrights.css',
                        'resources/src/mediawiki.special/watchlist.css',
                        'resources/src/mediawiki.special/block.less',
+                       'resources/src/mediawiki.special/listFiles.less',
                        'resources/src/mediawiki.special/blocklist.less',
                ],
                'targets' => [ 'desktop', 'mobile' ],
diff --git a/resources/src/mediawiki.special/listFiles.less b/resources/src/mediawiki.special/listFiles.less
new file mode 100644 (file)
index 0000000..c54eb09
--- /dev/null
@@ -0,0 +1,43 @@
+@import 'mediawiki.ui/variables';
+
+// On mobile devices the table layout is collapsed.
+@media all and ( max-width: @width-breakpoint-tablet ) {
+       .mw-special-Listfiles {
+               // stylelint-disable selector-class-pattern
+               thead,
+               .TablePager_col_count,
+               .TablePager_col_img_size,
+               .TablePager_col_img_name,
+               .TablePager_col_img_timestamp {
+                       display: none;
+               }
+
+               tbody,
+               tr,
+               td,
+               .mw-datatable,
+               .TablePager_col_img_description,
+               .TablePager_col_thumb {
+                       display: block;
+               }
+
+               .mw-datatable,
+               .mw-datatable th,
+               .mw-datatable td {
+                       border: 0;
+               }
+
+               .TablePager_col_img_user_text,
+               .TablePager_col_img_description {
+                       color: @colorGray5;
+                       margin: 0.5em 0 0;
+                       padding-bottom: 40px;
+                       line-height: 1.5;
+               }
+
+               .TablePager_col_img_user_text {
+                       padding: 0;
+               }
+               // stylelint-enable selector-class-pattern
+       }
+}
index d563235..bd3cebf 100644 (file)
@@ -25601,7 +25601,7 @@ __TOC__
 !! article
 MediaWiki:T34057
 !! text
-== {{int:headline_sample}} ==
+== {{int:ok}} ==
 !! endarticle
 
 !! test
@@ -25611,7 +25611,7 @@ title=[[Main Page]]
 !! wikitext
 {{int:T34057}}
 !! html
-<h2><span class="mw-headline" id="Headline_text">Headline text</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Main_Page&amp;action=edit&amp;section=1" title="Edit section: Headline text">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
+<h2><span class="mw-headline" id="OK">OK</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/index.php?title=Main_Page&amp;action=edit&amp;section=1" title="Edit section: OK">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
 !! end
 
 !! test
index a310242..d5bbb11 100644 (file)
@@ -3,7 +3,7 @@
 namespace MediaWiki\Tests\Rest\BasicAccess;
 
 use GuzzleHttp\Psr7\Uri;
-use MediaWiki\MediaWikiServices;
+use MediaWiki\Permissions\PermissionManager;
 use MediaWiki\Rest\BasicAccess\MWBasicAuthorizer;
 use MediaWiki\Rest\Handler;
 use MediaWiki\Rest\RequestData;
@@ -26,20 +26,18 @@ use Wikimedia\ObjectFactory;
 class MWBasicRequestAuthorizerTest extends MediaWikiTestCase {
        private function createRouter( $userRights, $request ) {
                $user = User::newFromName( 'Test user' );
-               // Don't allow the rights to everybody so that user rights kick in.
-               $this->mergeMwGlobalArrayValue( 'wgGroupPermissions', [ '*' => $userRights ] );
-               $this->overrideUserPermissions(
-                       $user,
-                       array_keys( array_filter( $userRights ), function ( $value ) {
-                               return $value === true;
-                       } )
-               );
-
-               global $IP;
-
                $objectFactory = new ObjectFactory(
                        $this->getMockForAbstractClass( ContainerInterface::class )
                );
+               $permissionManager = $this->createMock( PermissionManager::class );
+               // Don't allow the rights to everybody so that user rights kick in.
+               $permissionManager->method( 'isEveryoneAllowed' )->willReturn( false );
+               $permissionManager->method( 'userHasRight' )
+                       ->will( $this->returnCallback( function ( $user, $action ) use ( $userRights ) {
+                               return isset( $userRights[$action] ) && $userRights[$action];
+                       } ) );
+
+               global $IP;
 
                return new Router(
                        [ "$IP/tests/phpunit/unit/includes/Rest/testRoutes.json" ],
@@ -47,9 +45,9 @@ class MWBasicRequestAuthorizerTest extends MediaWikiTestCase {
                        '/rest',
                        new \EmptyBagOStuff(),
                        new ResponseFactory( [] ),
-                       new MWBasicAuthorizer( $user, MediaWikiServices::getInstance()->getPermissionManager() ),
+                       new MWBasicAuthorizer( $user, $permissionManager ),
                        $objectFactory,
-                       new Validator( $objectFactory, $request, $user )
+                       new Validator( $objectFactory, $permissionManager, $request, $user )
                );
        }
 
index 1c9bc41..d05c797 100644 (file)
@@ -5,6 +5,7 @@ namespace MediaWiki\Tests\Rest;
 use EmptyBagOStuff;
 use GuzzleHttp\Psr7\Uri;
 use GuzzleHttp\Psr7\Stream;
+use MediaWiki\Permissions\PermissionManager;
 use MediaWiki\Rest\BasicAccess\StaticBasicAuthorizer;
 use MediaWiki\Rest\Handler;
 use MediaWiki\Rest\EntryPoint;
@@ -32,6 +33,7 @@ class EntryPointTest extends \MediaWikiTestCase {
                $objectFactory = new ObjectFactory(
                        $this->getMockForAbstractClass( ContainerInterface::class )
                );
+               $permissionManager = $this->createMock( PermissionManager::class );
 
                return new Router(
                        [ "$IP/tests/phpunit/unit/includes/Rest/testRoutes.json" ],
@@ -41,7 +43,7 @@ class EntryPointTest extends \MediaWikiTestCase {
                        new ResponseFactory( [] ),
                        new StaticBasicAuthorizer(),
                        $objectFactory,
-                       new Validator( $objectFactory, $request, new User )
+                       new Validator( $objectFactory, $permissionManager, $request, new User )
                );
        }
 
diff --git a/tests/phpunit/includes/api/ApiQueryBlockInfoTraitTest.php b/tests/phpunit/includes/api/ApiQueryBlockInfoTraitTest.php
new file mode 100644 (file)
index 0000000..cded9dd
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+
+use MediaWiki\Block\DatabaseBlock;
+use MediaWiki\MediaWikiServices;
+use Wikimedia\TestingAccessWrapper;
+use Wikimedia\Timestamp\ConvertibleTimestamp;
+
+/**
+ * @covers ApiQueryBlockInfoTrait
+ */
+class ApiQueryBlockInfoTraitTest extends MediaWikiTestCase {
+
+       public function testUsesApiBlockInfoTrait() {
+               $this->assertTrue( method_exists( ApiQueryBlockInfoTrait::class, 'getBlockDetails' ),
+                       'ApiQueryBlockInfoTrait::getBlockDetails exists' );
+       }
+
+       /**
+        * @dataProvider provideAddBlockInfoToQuery
+        */
+       public function testAddBlockInfoToQuery( $args, $expect ) {
+               // Fake timestamp to show up in the queries
+               $reset = ConvertibleTimestamp::setFakeTime( '20190101000000' );
+
+               $data = [];
+
+               $mock = $this->getMockForTrait( ApiQueryBlockInfoTrait::class );
+               $mock->method( 'getDB' )->willReturn( wfGetDB( DB_REPLICA ) );
+               $mock->method( 'getPermissionManager' )
+                       ->willReturn( MediaWikiServices::getInstance()->getPermissionManager() );
+               $mock->method( 'getUser' )
+                       ->willReturn( $this->getMutableTestUser()->getUser() );
+               $mock->method( 'addTables' )->willReturnCallback( function ( $v ) use ( &$data ) {
+                       $data['tables'] = array_merge( $data['tables'] ?? [], (array)$v );
+               } );
+               $mock->method( 'addFields' )->willReturnCallback( function ( $v ) use ( &$data ) {
+                       $data['fields'] = array_merge( $data['fields'] ?? [], (array)$v );
+               } );
+               $mock->method( 'addWhere' )->willReturnCallback( function ( $v ) use ( &$data ) {
+                       $data['where'] = array_merge( $data['where'] ?? [], (array)$v );
+               } );
+               $mock->method( 'addJoinConds' )->willReturnCallback( function ( $v ) use ( &$data ) {
+                       $data['joins'] = array_merge( $data['joins'] ?? [], (array)$v );
+               } );
+
+               TestingAccessWrapper::newFromObject( $mock )->addBlockInfoToQuery( ...$args );
+               $this->assertEquals( $expect, $data );
+       }
+
+       public function provideAddBlockInfoToQuery() {
+               $queryInfo = DatabaseBlock::getQueryInfo();
+
+               $db = wfGetDB( DB_REPLICA );
+               $ts = $db->addQuotes( $db->timestamp( '20190101000000' ) );
+
+               return [
+                       [ [ false ], [
+                               'tables' => [ 'blk' => [ 'ipblocks' ] ],
+                               'fields' => [ 'ipb_deleted' ],
+                               'where' => [ 'ipb_deleted = 0 OR ipb_deleted IS NULL' ],
+                               'joins' => [
+                                       'blk' => [ 'LEFT JOIN', [ 'ipb_user=user_id', "ipb_expiry > $ts" ] ]
+                               ],
+                       ] ],
+
+                       [ [ true ], [
+                               'tables' => [ 'blk' => $queryInfo['tables'] ],
+                               'fields' => $queryInfo['fields'],
+                               'where' => [ 'ipb_deleted = 0 OR ipb_deleted IS NULL' ],
+                               'joins' => $queryInfo['joins'] + [
+                                       'blk' => [ 'LEFT JOIN', [ 'ipb_user=user_id', "ipb_expiry > $ts" ] ]
+                               ],
+                       ] ],
+               ];
+       }
+
+}
index 7acd237..8f37805 100644 (file)
@@ -193,4 +193,26 @@ class LocalFileTest extends MediaWikiTestCase {
                        'wfLocalFile() returns LocalFile for valid Titles'
                );
        }
+
+       /**
+        * @covers File::getUser
+        */
+       public function testGetUserForNonExistingFile() {
+               $this->assertSame( 'Unknown user', $this->file_hl0->getUser() );
+               $this->assertSame( 0, $this->file_hl0->getUser( 'id' ) );
+       }
+
+       /**
+        * @covers File::getUser
+        */
+       public function testDescriptionShortUrlForNonExistingFile() {
+               $this->assertNull( $this->file_hl0->getDescriptionShortUrl() );
+       }
+
+       /**
+        * @covers File::getUser
+        */
+       public function testDescriptionTextForNonExistingFile() {
+               $this->assertFalse( $this->file_hl0->getDescriptionText() );
+       }
 }
index ac988e6..076bf52 100644 (file)
@@ -1981,6 +1981,8 @@ class WANObjectCacheTest extends PHPUnit\Framework\TestCase {
                        [ 'domain:page:5', 'page' ],
                        [ 'domain:main-key', 'main-key' ],
                        [ 'domain:page:history', 'page' ],
+                       // Regression test for T232907
+                       [ 'domain:foo-bar-1.2:abc:v2', 'foo-bar-1_2' ],
                        [ 'missingdomainkey', 'missingdomainkey' ]
                ];
        }
index 7d682fd..ca3cf10 100644 (file)
@@ -4,6 +4,7 @@ namespace MediaWiki\Tests\Rest\Handler;
 
 use EmptyBagOStuff;
 use GuzzleHttp\Psr7\Uri;
+use MediaWiki\Permissions\PermissionManager;
 use MediaWiki\Rest\BasicAccess\StaticBasicAuthorizer;
 use MediaWiki\Rest\RequestData;
 use MediaWiki\Rest\ResponseFactory;
@@ -55,7 +56,7 @@ class HelloHandlerTest extends \MediaWikiUnitTestCase {
                $objectFactory = new ObjectFactory(
                        $this->getMockForAbstractClass( ContainerInterface::class )
                );
-
+               $permissionManager = $this->createMock( PermissionManager::class );
                $request = new RequestData( $requestInfo );
                $router = new Router(
                        [ __DIR__ . '/../testRoutes.json' ],
@@ -65,7 +66,7 @@ class HelloHandlerTest extends \MediaWikiUnitTestCase {
                        new ResponseFactory( [] ),
                        new StaticBasicAuthorizer(),
                        $objectFactory,
-                       new Validator( $objectFactory, $request, new User )
+                       new Validator( $objectFactory, $permissionManager, $request, new User )
                );
                $response = $router->execute( $request );
                if ( isset( $responseInfo['statusCode'] ) ) {
index 58039ea..270bcfc 100644 (file)
@@ -3,6 +3,7 @@
 namespace MediaWiki\Tests\Rest;
 
 use GuzzleHttp\Psr7\Uri;
+use MediaWiki\Permissions\PermissionManager;
 use MediaWiki\Rest\BasicAccess\StaticBasicAuthorizer;
 use MediaWiki\Rest\Handler;
 use MediaWiki\Rest\HttpException;
@@ -24,6 +25,7 @@ class RouterTest extends \MediaWikiUnitTestCase {
                $objectFactory = new ObjectFactory(
                        $this->getMockForAbstractClass( ContainerInterface::class )
                );
+               $permissionManager = $this->createMock( PermissionManager::class );
                return new Router(
                        [ __DIR__ . '/testRoutes.json' ],
                        [],
@@ -32,7 +34,7 @@ class RouterTest extends \MediaWikiUnitTestCase {
                        new ResponseFactory( [] ),
                        new StaticBasicAuthorizer( $authError ),
                        $objectFactory,
-                       new Validator( $objectFactory, $request, new User )
+                       new Validator( $objectFactory, $permissionManager, $request, new User )
                );
        }
 
index ed6c78a..79356a2 100644 (file)
@@ -1,3 +1,7 @@
+## 0.5.0 / 2019-09-18
+
+* Api: Added `bot()` method.
+
 ## 0.4.0 / 2019-07-18
 
 * Util: Added a `waitForModuleState()` method.
index 357fbd9..d2698f7 100644 (file)
@@ -27,6 +27,7 @@ which typically come from `MEDIAWIKI_USER` and `MEDIAWIKI_PASSWORD` environment
 * `createAccount(string username, string password)`
 * `blockUser([ string username [, string expiry ] ])`
 * `unblockUser([ string username ])`
+* `bot([string username [, string password [, string baseUrl ] ] ])`
 
 ### RunJobs
 
index 423487f..58a5390 100644 (file)
@@ -1,6 +1,6 @@
 {
   "name": "wdio-mediawiki",
-  "version": "0.4.0",
+  "version": "0.5.0",
   "description": "WebdriverIO plugin for testing a MediaWiki site.",
   "homepage": "https://gerrit.wikimedia.org/g/mediawiki/core/+/master/tests/selenium/wdio-mediawiki/",
   "license": "MIT",