php: hhvm-3.18
allow_failures:
- php: 7.2
- - env: dbtype=postgres dbuser=travis
- php: hhvm-3.18
- php: hhvm-3.21
- php: hhvm-3.24
* The wfUseMW function, soft-deprecated in 1.26, is now hard deprecated.
* All MagicWord static methods are now deprecated. Use the MagicWordFactory
methods instead.
+* PasswordFactory::init is deprecated. To get a password factory with the
+ standard configuration, use MediaWikiServices::getPasswordFactory.
=== Other changes in 1.32 ===
* (T198811) The following tables have had their UNIQUE indexes turned into
'DeleteEqualMessages' => __DIR__ . '/maintenance/deleteEqualMessages.php',
'DeleteFileOp' => __DIR__ . '/includes/libs/filebackend/fileop/DeleteFileOp.php',
'DeleteLinksJob' => __DIR__ . '/includes/jobqueue/jobs/DeleteLinksJob.php',
+ 'DeleteLocalPasswords' => __DIR__ . '/maintenance/includes/DeleteLocalPasswords.php',
'DeleteLogFormatter' => __DIR__ . '/includes/logging/DeleteLogFormatter.php',
'DeleteOldRevisions' => __DIR__ . '/maintenance/deleteOldRevisions.php',
'DeleteOrphanedRevisions' => __DIR__ . '/maintenance/deleteOrphanedRevisions.php',
use ObjectCache;
use Parser;
use ParserCache;
+use PasswordFactory;
use ProxyLookup;
use SearchEngine;
use SearchEngineConfig;
return $this->getService( 'MagicWordFactory' );
}
+ /**
+ * @since 1.32
+ * @return \Language
+ */
+ public function getContentLanguage() {
+ return $this->getService( 'ContentLanguage' );
+ }
+
+ /**
+ * @since 1.32
+ * @return PasswordFactory
+ */
+ public function getPasswordFactory() {
+ return $this->getService( 'PasswordFactory' );
+ }
+
///////////////////////////////////////////////////////////////////////////
// NOTE: When adding a service getter here, don't forget to add a test
// case for it in MediaWikiServicesTest::provideGetters() and in
* @var bool Is the displayed content related to the source of the
* corresponding wiki article.
*/
- private $mIsarticle = false;
+ private $mIsArticle = false;
/** @var bool Stores "article flag" toggle. */
private $mIsArticleRelated = true;
* corresponding article on the wiki
* Setting true will cause the change "article related" toggle to true
*
- * @param bool $v
+ * @param bool $newVal
*/
- public function setArticleFlag( $v ) {
- $this->mIsarticle = $v;
- if ( $v ) {
- $this->mIsArticleRelated = $v;
+ public function setArticleFlag( $newVal ) {
+ $this->mIsArticle = $newVal;
+ if ( $newVal ) {
+ $this->mIsArticleRelated = $newVal;
}
}
* @return bool
*/
public function isArticle() {
- return $this->mIsarticle;
+ return $this->mIsArticle;
}
/**
* Set whether this page is related an article on the wiki
* Setting false will cause the change of "article flag" toggle to false
*
- * @param bool $v
+ * @param bool $newVal
*/
- public function setArticleRelated( $v ) {
- $this->mIsArticleRelated = $v;
- if ( !$v ) {
- $this->mIsarticle = false;
+ public function setArticleRelated( $newVal ) {
+ $this->mIsArticleRelated = $newVal;
+ if ( !$newVal ) {
+ $this->mIsArticle = false;
}
}
public function addCategoryLinks( array $categories ) {
global $wgContLang;
- if ( !is_array( $categories ) || count( $categories ) == 0 ) {
+ if ( !$categories ) {
return;
}
* Set the revision ID which will be seen by the wiki text parser
* for things such as embedded {{REVISIONID}} variable use.
*
- * @param int|null $revid An positive integer, or null
+ * @param int|null $revid A positive integer, or null
* @return mixed Previous value
*/
public function setRevisionId( $revid ) {
$val = is_null( $revid ) ? null : intval( $revid );
- return wfSetVar( $this->mRevisionId, $val );
+ return wfSetVar( $this->mRevisionId, $val, true );
}
/**
* @return mixed Previous value
*/
public function setRevisionTimestamp( $timestamp ) {
- return wfSetVar( $this->mRevisionTimestamp, $timestamp );
+ return wfSetVar( $this->mRevisionTimestamp, $timestamp, true );
}
/**
/**
* Set the displayed file version
*
- * @param File|bool $file
+ * @param File|null $file
* @return mixed Previous value
*/
public function setFileVersion( $file ) {
* Add wikitext with a custom Title object
*
* @param string $text Wikitext
- * @param Title &$title
+ * @param Title $title
* @param bool $linestart Is this the start of a line?
*/
- public function addWikiTextWithTitle( $text, &$title, $linestart = true ) {
+ public function addWikiTextWithTitle( $text, Title $title, $linestart = true ) {
$this->addWikiTextTitle( $text, $title, $linestart );
}
* Add wikitext with a custom Title object and tidy enabled.
*
* @param string $text Wikitext
- * @param Title &$title
+ * @param Title $title
* @param bool $linestart Is this the start of a line?
*/
- function addWikiTextTitleTidy( $text, &$title, $linestart = true ) {
+ function addWikiTextTitleTidy( $text, Title $title, $linestart = true ) {
$this->addWikiTextTitle( $text, $title, $linestart, true );
}
},
'InterwikiLookup' => function ( MediaWikiServices $services ) {
- global $wgContLang; // TODO: manage $wgContLang as a service
$config = $services->getMainConfig();
return new ClassicInterwikiLookup(
- $wgContLang,
+ $services->getContentLanguage(),
$services->getMainWANObjectCache(),
$config->get( 'InterwikiExpiry' ),
$config->get( 'InterwikiCache' ),
},
'SearchEngineConfig' => function ( MediaWikiServices $services ) {
- global $wgContLang;
- return new SearchEngineConfig( $services->getMainConfig(), $wgContLang );
+ return new SearchEngineConfig( $services->getMainConfig(),
+ $services->getContentLanguage() );
},
'SkinFactory' => function ( MediaWikiServices $services ) {
},
'_MediaWikiTitleCodec' => function ( MediaWikiServices $services ) {
- global $wgContLang;
-
return new MediaWikiTitleCodec(
- $wgContLang,
+ $services->getContentLanguage(),
$services->getGenderCache(),
$services->getMainConfig()->get( 'LocalInterwikis' )
);
},
'BlobStoreFactory' => function ( MediaWikiServices $services ) {
- global $wgContLang;
return new BlobStoreFactory(
$services->getDBLoadBalancer(),
$services->getMainWANObjectCache(),
$services->getMainConfig(),
- $wgContLang
+ $services->getContentLanguage()
);
},
},
'PreferencesFactory' => function ( MediaWikiServices $services ) {
- global $wgContLang;
$authManager = AuthManager::singleton();
$linkRenderer = $services->getLinkRendererFactory()->create();
$config = $services->getMainConfig();
- $factory = new DefaultPreferencesFactory( $config, $wgContLang, $authManager, $linkRenderer );
+ $factory = new DefaultPreferencesFactory( $config, $services->getContentLanguage(),
+ $authManager, $linkRenderer );
$factory->setLogger( LoggerFactory::getInstance( 'preferences' ) );
return $factory;
},
'CommentStore' => function ( MediaWikiServices $services ) {
- global $wgContLang;
return new CommentStore(
- $wgContLang,
+ $services->getContentLanguage(),
$services->getMainConfig()->get( 'CommentTableSchemaMigrationStage' )
);
},
},
'MagicWordFactory' => function ( MediaWikiServices $services ) {
- global $wgContLang;
- return new MagicWordFactory( $wgContLang );
+ return new MagicWordFactory( $services->getContentLanguage() );
+ },
+
+ 'ContentLanguage' => function ( MediaWikiServices $services ) {
+ return Language::factory( $services->getMainConfig()->get( 'LanguageCode' ) );
+ },
+
+ 'PasswordFactory' => function ( MediaWikiServices $services ) {
+ $config = $services->getMainConfig();
+ return new PasswordFactory(
+ $config->get( 'PasswordConfig' ),
+ $config->get( 'PasswordDefault' )
+ );
},
///////////////////////////////////////////////////////////////////////////
/**
* @var Language $wgContLang
+ * @deprecated since 1.32, use the ContentLanguage service directly
*/
-$wgContLang = Language::factory( $wgLanguageCode );
-$wgContLang->initContLang();
+$wgContLang = MediaWikiServices::getInstance()->getContentLanguage();
// Now that variant lists may be available...
$wgRequest->interpolateTitle();
$fit = $this->appendMagicWords( $p );
break;
case 'interwikimap':
- $filteriw = $params['filteriw'] ?? false;
- $fit = $this->appendInterwikiMap( $p, $filteriw );
+ $fit = $this->appendInterwikiMap( $p, $params['filteriw'] );
break;
case 'dbrepllag':
$fit = $this->appendDbReplLagInfo( $p, $params['showalldb'] );
$fit = $this->appendUploadDialog( $p );
break;
default:
- ApiBase::dieDebug( __METHOD__, "Unknown prop=$p" );
+ ApiBase::dieDebug( __METHOD__, "Unknown prop=$p" ); // @codeCoverageIgnore
}
if ( !$fit ) {
// Abuse siprop as a query-continue parameter
$data['phpversion'] = PHP_VERSION;
$data['phpsapi'] = PHP_SAPI;
if ( defined( 'HHVM_VERSION' ) ) {
- $data['hhvmversion'] = HHVM_VERSION;
+ $data['hhvmversion'] = HHVM_VERSION; // @codeCoverageIgnore
}
$data['dbtype'] = $config->get( 'DBtype' );
$data['dbversion'] = $this->getDB()->getServerVersion();
$tz = $config->get( 'Localtimezone' );
$offset = $config->get( 'LocalTZoffset' );
- if ( is_null( $tz ) ) {
- $tz = 'UTC';
- $offset = 0;
- } elseif ( is_null( $offset ) ) {
- $offset = 0;
- }
$data['timezone'] = $tz;
$data['timeoffset'] = intval( $offset );
$data['articlepath'] = $config->get( 'ArticlePath' );
}
protected function appendInterwikiMap( $property, $filter ) {
- $local = null;
if ( $filter === 'local' ) {
$local = 1;
} elseif ( $filter === '!local' ) {
$local = 0;
- } elseif ( $filter ) {
- ApiBase::dieDebug( __METHOD__, "Unknown filter=$filter" );
+ } else {
+ // $filter === null
+ $local = null;
}
$params = $this->extractRequestParams();
$url = $config->get( 'RightsUrl' );
}
$text = $config->get( 'RightsText' );
- if ( !$text && $title ) {
+ if ( $title && !strlen( $text ) ) {
$text = $title->getPrefixedText();
}
$data = [
- 'url' => $url ?: '',
- 'text' => $text ?: ''
+ 'url' => strlen( $url ) ? $url : '',
+ 'text' => strlen( $text ) ? $text : '',
];
return $this->getResult()->addValue( 'query', $property, $data );
public function appendExtensionTags( $property ) {
global $wgParser;
$wgParser->firstCallInit();
- $tags = array_map( [ $this, 'formatParserTags' ], $wgParser->getTags() );
+ $tags = array_map(
+ function ( $item ) {
+ return "<$item>";
+ },
+ $wgParser->getTags()
+ );
ApiResult::setArrayType( $tags, 'BCarray' );
ApiResult::setIndexedTagName( $tags, 't' );
return $this->getResult()->addValue( 'query', $property, $config );
}
- private function formatParserTags( $item ) {
- return "<{$item}>";
- }
-
public function appendSubscribedHooks( $property ) {
$hooks = $this->getConfig()->get( 'Hooks' );
$myWgHooks = $hooks;
*/
protected function getPasswordFactory() {
if ( $this->passwordFactory === null ) {
- $this->passwordFactory = new PasswordFactory();
- $this->passwordFactory->init( $this->config );
+ $this->passwordFactory = new PasswordFactory(
+ $this->config->get( 'PasswordConfig' ),
+ $this->config->get( 'PasswordDefault' )
+ );
}
return $this->passwordFactory;
}
public function getTimestamp( $rc ) {
// @todo FIXME: Hard coded ". .". Is there a message for this? Should there be?
return $this->message['semicolon-separator'] . '<span class="mw-changeslist-date">' .
- $this->getLanguage()->userTime(
+ htmlspecialchars( $this->getLanguage()->userTime(
$rc->mAttribs['rc_timestamp'],
$this->getUser()
- ) . '</span> <span class="mw-changeslist-separator">. .</span> ';
+ ) ) . '</span> <span class="mw-changeslist-separator">. .</span> ';
}
/**
* to avoid formatting for any particular user.
* @see getActionText()
* @return string Plain text
+ * @return-taint tainted
*/
public function getPlainActionText() {
$this->plaintext = true;
/**
* Gets the log action, including username.
* @return string HTML
+ * phan-taint-check gets very confused by $this->plaintext, so disable.
+ * @return-taint onlysafefor_html
*/
public function getActionText() {
if ( $this->canView( LogPage::DELETED_ACTION ) ) {
* Helper method for displaying restricted element.
* @param string $message
* @return string HTML or wiki text
+ * @return-taint onlysafefor_html
*/
protected function getRestrictedElement( $message ) {
if ( $this->plaintext ) {
return $this->context->msg( $key );
}
+ /**
+ * @param User $user
+ * @param int $toolFlags Combination of Linker::TOOL_LINKS_* flags
+ * @return string wikitext or html
+ * @return-taint onlysafefor_html
+ */
protected function makeUserLink( User $user, $toolFlags = 0 ) {
if ( $this->plaintext ) {
$element = $user->getName();
return $this->comment;
}
+ /**
+ * @return string
+ * @return-taint onlysafefor_html
+ */
protected function getActionMessage() {
$entry = $this->entry;
$action = LogPage::actionText(
public $mImageParams = [];
public $mImageParamsMagicArray = [];
public $mMarkerIndex = 0;
+ /**
+ * @var bool Whether firstCallInit still needs to be called
+ */
public $mFirstCall = true;
# Initialised by initialiseVariables()
* @private
*/
public function clearState() {
- if ( $this->mFirstCall ) {
- $this->firstCallInit();
- }
+ $this->firstCallInit();
$this->mOutput = new ParserOutput;
$this->mOptions->registerWatcher( [ $this->mOutput, 'recordOption' ] );
$this->mAutonumber = 0;
/**
* Mapping of password types to classes
+ *
* @var array
* @see PasswordFactory::register
* @see Setup.php
'' => [ 'type' => '', 'class' => InvalidPassword::class ],
];
+ /**
+ * Construct a new password factory.
+ * Most of the time you'll want to use MediaWikiServices::getPasswordFactory instead.
+ * @param array $config Mapping of password type => config
+ * @param string $default Default password type
+ * @see PasswordFactory::register
+ * @see PasswordFactory::setDefaultType
+ */
+ public function __construct( array $config = [], $default = '' ) {
+ foreach ( $config as $type => $options ) {
+ $this->register( $type, $options );
+ }
+
+ if ( $default !== '' ) {
+ $this->setDefaultType( $default );
+ }
+ }
+
/**
* Register a new type of password hash
*
- * @param string $type Unique type name for the hash
- * @param array $config Array of configuration options
+ * @param string $type Unique type name for the hash. Will be prefixed to the password hashes
+ * to identify what hashing method was used.
+ * @param array $config Array of configuration options. 'class' is required (the Password
+ * subclass name), everything else is passed to the constructor of that class.
*/
public function register( $type, array $config ) {
$config['type'] = $type;
/**
* Set the default password type
*
- * @throws InvalidArgumentException If the type is not registered
+ * This type will be used for creating new passwords when the type is not specified.
+ * Passwords of a different type will be considered outdated and in need of update.
+ *
* @param string $type Password hash type
+ * @throws InvalidArgumentException If the type is not registered
*/
public function setDefaultType( $type ) {
if ( !isset( $this->types[$type] ) ) {
}
/**
+ * @deprecated since 1.32 Initialize settings using the constructor
+ *
* Initialize the internal static variables using the global variables
*
* @param Config $config Configuration object to load data from
*/
use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
/**
* Let users manage bot passwords
} else {
$linkRenderer = $this->getLinkRenderer();
- $passwordFactory = new PasswordFactory();
- $passwordFactory->init( $this->getConfig() );
+ $passwordFactory = MediaWikiServices::getInstance()->getPasswordFactory();
$dbr = BotPassword::getDB( DB_REPLICA );
$res = $dbr->select(
if ( $this->operation === 'insert' || !empty( $data['resetPassword'] ) ) {
$this->password = BotPassword::generatePassword( $this->getConfig() );
- $passwordFactory = new PasswordFactory();
- $passwordFactory->init( RequestContext::getMain()->getConfig() );
+ $passwordFactory = MediaWikiServices::getInstance()->getPasswordFactory();
$password = $passwordFactory->newFromPlaintext( $this->password );
} else {
$password = null;
return PasswordFactory::newInvalidPassword();
}
- $passwordFactory = new \PasswordFactory();
- $passwordFactory->init( \RequestContext::getMain()->getConfig() );
+ $passwordFactory = MediaWikiServices::getInstance()->getPasswordFactory();
try {
return $passwordFactory->newFromCiphertext( $password );
} catch ( PasswordError $ex ) {
throw new MWException( "Invalid fallback sequence for language '$code'" );
}
+ /**
+ * Intended for tests that may change configuration in a way that invalidates caches.
+ *
+ * @since 1.32
+ */
+ public static function clearCaches() {
+ if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
+ throw new MWException( __METHOD__ . ' must not be used outside tests' );
+ }
+ self::$dataCache = null;
+ // Reinitialize $dataCache, since it's expected to always be available
+ self::getLocalisationCache();
+ self::$mLangObjCache = [];
+ self::$fallbackLanguageCache = [];
+ self::$grammarTransformations = null;
+ self::$languageNameCache = null;
+ }
+
/**
* Checks whether any localisation is available for that language tag
* in MediaWiki (MessagesXx.php exists).
--- /dev/null
+<?php
+/**
+ * Delete unused local passwords.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Maintenance
+ */
+
+require_once __DIR__ . '/includes/DeleteLocalPasswords.php';
+
+$maintClass = DeleteLocalPasswords::class;
+require_once RUN_MAINTENANCE_IF_MAIN;
--- /dev/null
+<?php
+/**
+ * Helper for deleting unused local passwords.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Maintenance
+ */
+
+use MediaWiki\MediaWikiServices;
+use Wikimedia\Rdbms\IDatabase;
+
+require_once __DIR__ . '/../Maintenance.php';
+
+/**
+ * Delete unused local passwords.
+ *
+ * Mainly intended to be used as a base class by authentication extensions to provide maintenance
+ * scripts which allow deleting local passwords for users who have another way of logging in.
+ * Such scripts would customize how to locate users who have other login methods and don't need
+ * local login anymore.
+ * Make sure to set LocalPasswordPrimaryAuthenticationProvider to loginOnly => true or disable it
+ * completely before running this, otherwise it might recreate passwords.
+ *
+ * This class can also be used directly to just delete all local passwords, or those for a specific
+ * user. Deleting all passwords is useful when the wiki has used local password login in the past
+ * but it has been disabled.
+ */
+class DeleteLocalPasswords extends Maintenance {
+ /** @var string|null User to run on, or null for all. */
+ protected $user;
+
+ /** @var int Number of deleted passwords. */
+ protected $total;
+
+ public function __construct() {
+ parent::__construct();
+ $this->mDescription = "Deletes local password for users.";
+ $this->setBatchSize( 1000 );
+
+ $this->addOption( 'user', 'If specified, only checks the given user', false, true );
+ $this->addOption( 'delete', 'Really delete. To prevent accidents, you must provide this flag.' );
+ $this->addOption( 'prefix', "Instead of deleting, make passwords invalid by prefixing with "
+ . "':null:'. Make sure PasswordConfig has a 'null' entry. This is meant for testing before "
+ . "hard delete." );
+ $this->addOption( 'unprefix', 'Instead of deleting, undo the effect of --prefix.' );
+ }
+
+ protected function initialize() {
+ if (
+ $this->hasOption( 'delete' ) + $this->hasOption( 'prefix' )
+ + $this->hasOption( 'unprefix' ) !== 1
+ ) {
+ $this->fatalError( "Exactly one of the 'delete', 'prefix', 'unprefix' options must be used\n" );
+ }
+ if ( $this->hasOption( 'prefix' ) || $this->hasOption( 'unprefix' ) ) {
+ $passwordHashTypes = MediaWikiServices::getInstance()->getPasswordFactory()->getTypes();
+ if (
+ !isset( $passwordHashTypes['null'] )
+ || $passwordHashTypes['null']['class'] !== InvalidPassword::class
+ ) {
+ $this->fatalError(
+<<<'ERROR'
+'null' password entry missing. To use password prefixing, add
+ $wgPasswordConfig['null'] = [ 'class' => InvalidPassword::class ];
+to your configuration (and remove once the passwords were deleted).
+ERROR
+ );
+ }
+ }
+
+ $user = $this->getOption( 'user', false );
+ if ( $user !== false ) {
+ $this->user = User::getCanonicalName( $user );
+ if ( $this->user === false ) {
+ $this->fatalError( "Invalid user name\n" );
+ }
+ }
+ }
+
+ public function execute() {
+ $this->initialize();
+
+ foreach ( $this->getUserBatches() as $userBatch ) {
+ $this->processUsers( $userBatch, $this->getUserDB() );
+ }
+
+ $this->output( "done. (wrote $this->total rows)\n" );
+ }
+
+ /**
+ * Get the master DB handle for the current user batch. This is provided for the benefit
+ * of authentication extensions which subclass this and work with wiki farms.
+ */
+ protected function getUserDB() {
+ return $this->getDB( DB_MASTER );
+ }
+
+ protected function processUsers( array $userBatch, IDatabase $dbw ) {
+ if ( !$userBatch ) {
+ return;
+ }
+ if ( $this->getOption( 'delete' ) ) {
+ $dbw->update( 'user',
+ [ 'user_password' => PasswordFactory::newInvalidPassword()->toString() ],
+ [ 'user_name' => $userBatch ],
+ __METHOD__
+ );
+ } elseif ( $this->getOption( 'prefix' ) ) {
+ $dbw->update( 'user',
+ [ 'user_password = ' . $dbw->buildConcat( [ $dbw->addQuotes( ':null:' ),
+ 'user_password' ] ) ],
+ [
+ 'NOT (user_password ' . $dbw->buildLike( ':null:', $dbw->anyString() ) . ')',
+ "user_password != " . $dbw->addQuotes( PasswordFactory::newInvalidPassword()->toString() ),
+ 'user_password IS NOT NULL',
+ 'user_name' => $userBatch,
+ ],
+ __METHOD__
+ );
+ } elseif ( $this->getOption( 'unprefix' ) ) {
+ $dbw->update( 'user',
+ [ 'user_password = ' . $dbw->buildSubString( 'user_password', strlen( ':null:' ) + 1 ) ],
+ [
+ 'user_password ' . $dbw->buildLike( ':null:', $dbw->anyString() ),
+ 'user_name' => $userBatch,
+ ],
+ __METHOD__
+ );
+ }
+ $this->total += $dbw->affectedRows();
+ }
+
+ /**
+ * This method iterates through the requested users and returns their names in batches of
+ * self::$mBatchSize.
+ *
+ * Subclasses should reimplement this and locate users who use the specific authentication
+ * method. The default implementation just iterates through all users. Extensions that work
+ * with wikifarm should also update self::getUserDB() as necessary.
+ * @return Generator
+ */
+ protected function getUserBatches() {
+ if ( !is_null( $this->user ) ) {
+ $this->output( "\t ... querying '$this->user'\n" );
+ yield [ $this->user ];
+ return;
+ }
+
+ $lastUsername = '';
+ $dbw = $this->getDB( DB_MASTER );
+ do {
+ $this->output( "\t ... querying from '$lastUsername'\n" );
+ $users = $dbw->selectFieldValues(
+ 'user',
+ 'user_name',
+ [
+ 'user_name > ' .$dbw->addQuotes( $lastUsername ),
+ ],
+ __METHOD__,
+ [
+ 'LIMIT' => $this->getBatchSize(),
+ 'ORDER BY' => 'user_name ASC',
+ ]
+ );
+ if ( $users ) {
+ yield $users;
+ $lastUsername = end( $users );
+ }
+ } while ( count( $users ) === $this->getBatchSize() );
+ }
+}
* Rebuild pass 3: Insert `recentchanges` entries for action logs.
*/
private function rebuildRecentChangesTablePass3( ILBFactory $lbFactory ) {
- global $wgLogRestrictions;
+ global $wgLogRestrictions, $wgFilterLogTypes;
$dbw = $this->getDB( DB_MASTER );
$commentStore = CommentStore::getStore();
+ $nonRCLogs = array_merge( array_keys( $wgLogRestrictions ),
+ array_keys( $wgFilterLogTypes ),
+ [ 'create' ] );
$this->output( "Loading from user, page, and logging tables...\n" );
[
'log_timestamp > ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffFrom ) ),
'log_timestamp < ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffTo ) ),
- // Some logs don't go in RC since they are private.
- // @FIXME: core/extensions also have spammy logs that don't go in RC.
- 'log_type' => array_diff( LogPage::validTypes(), array_keys( $wgLogRestrictions ) ),
+ // Some logs don't go in RC since they are private, or are included in the filterable log types.
+ 'log_type' => array_diff( LogPage::validTypes(), $nonRCLogs ),
],
__METHOD__,
[ 'ORDER BY' => 'log_timestamp DESC' ],
<?php
-
-use MediaWiki\MediaWikiServices;
-
/**
* Maintenance script to wrap all old-style passwords in a layered type
*
* @file
* @ingroup Maintenance
*/
+
require_once __DIR__ . '/Maintenance.php';
+use MediaWiki\MediaWikiServices;
+
/**
* Maintenance script to wrap all passwords of a certain type in a specified layered
* type that wraps around the old type.
}
public function execute() {
- $passwordFactory = new PasswordFactory();
- $passwordFactory->init( RequestContext::getMain()->getConfig() );
+ $passwordFactory = MediaWikiServices::getInstance()->getPasswordFactory();
$typeInfo = $passwordFactory->getTypes();
$layeredType = $this->getOption( 'type' );
// any live Language object, both on setup and teardown
$reset = function () {
MWNamespace::clearCaches();
- $GLOBALS['wgContLang']->resetNamespaces();
+ MediaWikiServices::getInstance()->getContentLanguage()->resetNamespaces();
};
$setup[] = $reset;
$teardown[] = $reset;
$lang = Language::factory( $langCode );
$lang->resetNamespaces();
$setup['wgContLang'] = $lang;
+ $setup[] = function () use ( $lang ) {
+ MediaWikiServices::getInstance()->disableService( 'ContentLanguage' );
+ MediaWikiServices::getInstance()->redefineService(
+ 'ContentLanguage',
+ function () use ( $lang ) {
+ return $lang;
+ }
+ );
+ };
+ $teardown[] = function () {
+ MediaWikiServices::getInstance()->resetServiceForTesting( 'ContentLanguage' );
+ };
$reset = function () {
MediaWikiServices::getInstance()->resetServiceForTesting( 'MagicWordFactory' );
$this->resetTitleServices();
* @param array $articles Article info array from TestFileReader
*/
public function addArticles( $articles ) {
- global $wgContLang;
$setup = [];
$teardown = [];
// Be sure ParserTestRunner::addArticle has correct language set,
// so that system messages get into the right language cache
- if ( $wgContLang->getCode() !== 'en' ) {
+ if ( MediaWikiServices::getInstance()->getContentLanguage()->getCode() !== 'en' ) {
$setup['wgLanguageCode'] = 'en';
- $setup['wgContLang'] = Language::factory( 'en' );
+ $lang = Language::factory( 'en' );
+ $setup['wgContLang'] = $lang;
+ $setup[] = function () use ( $lang ) {
+ $services = MediaWikiServices::getInstance();
+ $services->disableService( 'ContentLanguage' );
+ $services->redefineService( 'ContentLanguage', function () use ( $lang ) {
+ return $lang;
+ } );
+ };
+ $teardown[] = function () {
+ MediaWikiServices::getInstance()->resetServiceForTesting( 'ContentLanguage' );
+ };
}
// Add special namespaces, in case that hasn't been done by staticSetup() yet
*/
private $mwGlobalsToUnset = [];
+ /**
+ * Holds original contents of interwiki table
+ * @var IResultWrapper
+ */
+ private $interwikiTable = null;
+
/**
* Holds original loggers which have been replaced by setLogger()
* @var LoggerInterface[]
}
}
+ // Store contents of interwiki table in case it changes. Unfortunately, we seem to have no
+ // way to do this only when needed, because tablesUsed can be changed mid-test.
+ if ( $this->db ) {
+ $this->interwikiTable = $this->db->select( 'interwiki', '*', '', __METHOD__ );
+ }
+
// Reset all caches between tests.
$this->doLightweightServiceReset();
foreach ( $this->mwGlobalsToUnset as $value ) {
unset( $GLOBALS[$value] );
}
+ if (
+ array_key_exists( 'wgExtraNamespaces', $this->mwGlobals ) ||
+ in_array( 'wgExtraNamespaces', $this->mwGlobalsToUnset )
+ ) {
+ $this->resetNamespaces();
+ }
$this->mwGlobals = [];
$this->mwGlobalsToUnset = [];
$this->restoreLoggers();
return $object;
}
);
+
+ if ( $name === 'ContentLanguage' ) {
+ $this->doSetMwGlobals( [ 'wgContLang' => $object ] );
+ }
}
/**
$pairs = [ $pairs => $value ];
}
+ if ( isset( $pairs['wgContLang'] ) ) {
+ throw new MWException(
+ 'No setting $wgContLang, use setContentLang() or setService( \'ContentLanguage\' )'
+ );
+ }
+
+ $this->doSetMwGlobals( $pairs, $value );
+ }
+
+ /**
+ * An internal method that allows setService() to set globals that tests are not supposed to
+ * touch.
+ */
+ private function doSetMwGlobals( $pairs, $value = null ) {
$this->stashMwGlobals( array_keys( $pairs ) );
foreach ( $pairs as $key => $value ) {
$GLOBALS[$key] = $value;
}
+
+ if ( array_key_exists( 'wgExtraNamespaces', $pairs ) ) {
+ $this->resetNamespaces();
+ }
+ }
+
+ /**
+ * Must be called whenever namespaces are changed, e.g., $wgExtraNamespaces is altered.
+ * Otherwise old namespace data will lurk and cause bugs.
+ */
+ private function resetNamespaces() {
+ MWNamespace::clearCaches();
+ Language::clearCaches();
+
+ // We can't have the TitleFormatter holding on to an old Language object either
+ // @todo We shouldn't need to reset all the aliases here.
+ $services = MediaWikiServices::getInstance();
+ $services->resetServiceForTesting( 'TitleFormatter' );
+ $services->resetServiceForTesting( 'TitleParser' );
+ $services->resetServiceForTesting( '_MediaWikiTitleCodec' );
}
/**
$langCode = $lang;
$langObj = Language::factory( $langCode );
}
- $this->setMwGlobals( [
- 'wgLanguageCode' => $langCode,
- 'wgContLang' => $langObj,
- ] );
+ $this->setMwGlobals( 'wgLanguageCode', $langCode );
+ $this->setService( 'ContentLanguage', $langObj );
}
/**
$truncate = in_array( $db->getType(), [ 'oracle', 'mysql' ] );
foreach ( $tablesUsed as $tbl ) {
- // TODO: reset interwiki table to its original content.
- if ( $tbl == 'interwiki' ) {
- continue;
- }
-
if ( !$db->tableExists( $tbl ) ) {
continue;
}
$db->resetSequenceForTable( $tbl, __METHOD__ );
}
+ if ( $tbl === 'interwiki' ) {
+ if ( !$this->interwikiTable ) {
+ // @todo We should probably throw here, but this causes test failures that I
+ // can't figure out, so for now we silently continue.
+ continue;
+ }
+ $db->insert(
+ 'interwiki',
+ array_map( 'get_object_vars', iterator_to_array( $this->interwikiTable ) ),
+ __METHOD__
+ );
+ }
+
if ( $tbl === 'page' ) {
// Forget about the pages since they don't
// exist in the DB.
class EditPageTest extends MediaWikiLangTestCase {
protected function setUp() {
- global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
-
parent::setUp();
- $this->setContentLang( $wgContLang );
-
$this->setMwGlobals( [
- 'wgExtraNamespaces' => $wgExtraNamespaces,
- 'wgNamespaceContentModels' => $wgNamespaceContentModels,
- 'wgContentHandlers' => $wgContentHandlers,
+ 'wgExtraNamespaces' => [
+ 12312 => 'Dummy',
+ 12313 => 'Dummy_talk',
+ ],
+ 'wgNamespaceContentModels' => [ 12312 => 'testing' ],
] );
-
- $wgExtraNamespaces[12312] = 'Dummy';
- $wgExtraNamespaces[12313] = 'Dummy_talk';
-
- $wgNamespaceContentModels[12312] = "testing";
- $wgContentHandlers["testing"] = 'DummyContentHandlerForTesting';
-
- MWNamespace::clearCaches();
- $wgContLang->resetNamespaces(); # reset namespace cache
- }
-
- protected function tearDown() {
- global $wgContLang;
-
- MWNamespace::clearCaches();
- $wgContLang->resetNamespaces(); # reset namespace cache
- parent::tearDown();
+ $this->mergeMwGlobalArrayValue(
+ 'wgContentHandlers',
+ [ 'testing' => 'DummyContentHandlerForTesting' ]
+ );
}
/**
'ActorMigration' => [ 'ActorMigration', ActorMigration::class ],
'ConfigRepository' => [ 'ConfigRepository', \MediaWiki\Config\ConfigRepository::class ],
'MagicWordFactory' => [ 'MagicWordFactory', MagicWordFactory::class ],
+ 'ContentLanguage' => [ 'ContentLanguage', Language::class ],
+ 'PasswordFactory' => [ 'PasswordFactory', PasswordFactory::class ],
];
}
$this->assertSame( $key, $message->getKey() );
$this->assertSame( $params, $message->getParams() );
- $this->assertEquals( $expectedLang, $message->getLanguage() );
+ $this->assertSame( $expectedLang->getCode(), $message->getLanguage()->getCode() );
$messageSpecifier = $this->getMockForAbstractClass( MessageSpecifier::class );
$messageSpecifier->expects( $this->any() )
$this->assertSame( $key, $message->getKey() );
$this->assertSame( $params, $message->getParams() );
- $this->assertEquals( $expectedLang, $message->getLanguage() );
+ $this->assertSame( $expectedLang->getCode(), $message->getLanguage()->getCode() );
}
public static function provideConstructor() {
'' . $op->headElement( $op->getContext()->getSkin() ) );
}
+ /**
+ * @covers OutputPage::getHeadItemsArray
+ * @covers OutputPage::addParserOutputMetadata
+ */
+ public function testHeadItemsParserOutput() {
+ $op = $this->newInstance();
+ $stubPO1 = $this->createParserOutputStub( 'getHeadItems', [ 'a' => 'b' ] );
+ $op->addParserOutputMetadata( $stubPO1 );
+ $stubPO2 = $this->createParserOutputStub( 'getHeadItems',
+ [ 'c' => '<d>&', 'e' => 'f', 'a' => 'q' ] );
+ $op->addParserOutputMetadata( $stubPO2 );
+ $stubPO3 = $this->createParserOutputStub( 'getHeadItems', [ 'e' => 'g' ] );
+ $op->addParserOutputMetadata( $stubPO3 );
+ $stubPO4 = $this->createParserOutputStub( 'getHeadItems', [ 'x' ] );
+ $op->addParserOutputMetadata( $stubPO4 );
+
+ $this->assertSame( [ 'a' => 'q', 'c' => '<d>&', 'e' => 'g', 'x' ],
+ $op->getHeadItemsArray() );
+
+ $this->assertTrue( $op->hasHeadItem( 'a' ) );
+ $this->assertTrue( $op->hasHeadItem( 'c' ) );
+ $this->assertTrue( $op->hasHeadItem( 'e' ) );
+ $this->assertTrue( $op->hasHeadItem( '0' ) );
+ $this->assertFalse( $op->hasHeadItem( 'b' ) );
+
+ $this->assertContains( "\nq\n<d>&\ng\nx\n",
+ '' . $op->headElement( $op->getContext()->getSkin() ) );
+ }
+
/**
* @covers OutputPage::addBodyClasses
*/
*
* @covers OutputPage::buildBacklinkSubtitle
*/
- public function testBuildBacklinkSubtitle( Title $title, $query, $contains, $notContains ) {
+ public function testBuildBacklinkSubtitle( $titles, $queries, $contains, $notContains ) {
+ if ( count( $titles ) > 1 ) {
+ // Not applicable
+ $this->assertTrue( true );
+ return;
+ }
+
+ $title = Title::newFromText( $titles[0] );
+ $query = $queries[0];
+
$this->editPage( 'Page 1', '' );
$this->editPage( 'Page 2', '#REDIRECT [[Page 1]]' );
* @covers OutputPage::addBacklinkSubtitle
* @covers OutputPage::getSubtitle
*/
- public function testAddBacklinkSubtitle( Title $title, $query, $contains, $notContains ) {
+ public function testAddBacklinkSubtitle( $titles, $queries, $contains, $notContains ) {
$this->editPage( 'Page 1', '' );
$this->editPage( 'Page 2', '#REDIRECT [[Page 1]]' );
$op = $this->newInstance();
- $op->addBacklinkSubtitle( $title, $query );
+ foreach ( $titles as $i => $unused ) {
+ $op->addBacklinkSubtitle( Title::newFromText( $titles[$i] ), $queries[$i] );
+ }
$str = $op->getSubtitle();
}
public function provideBacklinkSubtitle() {
- $page1 = Title::newFromText( 'Page 1' );
- $page2 = Title::newFromText( 'Page 2' );
-
return [
- [ $page1, [], [ 'Page 1' ], [ 'redirect', 'Page 2' ] ],
- [ $page2, [], [ 'redirect=no' ], [ 'Page 1' ] ],
- [ $page1, [ 'action' => 'edit' ], [ 'action=edit' ], [] ],
+ [
+ [ 'Page 1' ],
+ [ [] ],
+ [ 'Page 1' ],
+ [ 'redirect', 'Page 2' ],
+ ],
+ [
+ [ 'Page 2' ],
+ [ [] ],
+ [ 'redirect=no' ],
+ [ 'Page 1' ],
+ ],
+ [
+ [ 'Page 1' ],
+ [ [ 'action' => 'edit' ] ],
+ [ 'action=edit' ],
+ [],
+ ],
+ [
+ [ 'Page 1', 'Page 2' ],
+ [ [], [] ],
+ [ 'Page 1', 'Page 2', "<br />\n\t\t\t\t" ],
+ [],
+ ],
// @todo Anything else to test?
];
}
/**
+ * @covers OutputPage::setPrintable
+ * @covers OutputPage::isPrintable
+ */
+ public function testPrintable() {
+ $op = $this->newInstance();
+
+ $this->assertFalse( $op->isPrintable() );
+
+ $op->setPrintable();
+
+ $this->assertTrue( $op->isPrintable() );
+ }
+
+ /**
+ * @covers OutputPage::disable
+ * @covers OutputPage::isDisabled
+ */
+ public function testDisable() {
+ $op = $this->newInstance();
+
+ $this->assertFalse( $op->isDisabled() );
+ $this->assertNotSame( '', $op->output( true ) );
+
+ $op->disable();
+
+ $this->assertTrue( $op->isDisabled() );
+ $this->assertSame( '', $op->output( true ) );
+ }
+
+ /**
+ * @covers OutputPage::showNewSectionLink
+ * @covers OutputPage::addParserOutputMetadata
+ */
+ public function testShowNewSectionLink() {
+ $op = $this->newInstance();
+
+ $this->assertFalse( $op->showNewSectionLink() );
+
+ $po = new ParserOutput();
+ $po->setNewSection( true );
+ $op->addParserOutputMetadata( $po );
+
+ $this->assertTrue( $op->showNewSectionLink() );
+ }
+
+ /**
+ * @covers OutputPage::forceHideNewSectionLink
+ * @covers OutputPage::addParserOutputMetadata
+ */
+ public function testForceHideNewSectionLink() {
+ $op = $this->newInstance();
+
+ $this->assertFalse( $op->forceHideNewSectionLink() );
+
+ $po = new ParserOutput();
+ $po->hideNewSection( true );
+ $op->addParserOutputMetadata( $po );
+
+ $this->assertTrue( $op->forceHideNewSectionLink() );
+ }
+
+ /**
+ * @covers OutputPage::setSyndicated
+ * @covers OutputPage::isSyndicated
+ */
+ public function testSetSyndicated() {
+ $op = $this->newInstance();
+ $this->assertFalse( $op->isSyndicated() );
+
+ $op->setSyndicated();
+ $this->assertTrue( $op->isSyndicated() );
+
+ $op->setSyndicated( false );
+ $this->assertFalse( $op->isSyndicated() );
+ }
+
+ /**
+ * @covers OutputPage::isSyndicated
+ * @covers OutputPage::setFeedAppendQuery
+ * @covers OutputPage::addFeedLink
+ * @covers OutputPage::getSyndicationLinks()
+ */
+ public function testFeedLinks() {
+ $op = $this->newInstance();
+ $this->assertSame( [], $op->getSyndicationLinks() );
+
+ $op->addFeedLink( 'not a supported format', 'abc' );
+ $this->assertFalse( $op->isSyndicated() );
+ $this->assertSame( [], $op->getSyndicationLinks() );
+
+ $feedTypes = $op->getConfig()->get( 'AdvertisedFeedTypes' );
+
+ $op->addFeedLink( $feedTypes[0], 'def' );
+ $this->assertTrue( $op->isSyndicated() );
+ $this->assertSame( [ $feedTypes[0] => 'def' ], $op->getSyndicationLinks() );
+
+ $op->setFeedAppendQuery( false );
+ $expected = [];
+ foreach ( $feedTypes as $type ) {
+ $expected[$type] = $op->getTitle()->getLocalURL( "feed=$type" );
+ }
+ $this->assertSame( $expected, $op->getSyndicationLinks() );
+
+ $op->setFeedAppendQuery( 'apples=oranges' );
+ foreach ( $feedTypes as $type ) {
+ $expected[$type] = $op->getTitle()->getLocalURL( "feed=$type&apples=oranges" );
+ }
+ $this->assertSame( $expected, $op->getSyndicationLinks() );
+ }
+
+ /**
+ * @covers OutputPage::setArticleFlag
+ * @covers OutputPage::isArticle
+ * @covers OutputPage::setArticleRelated
+ * @covers OutputPage::isArticleRelated
+ */
+ function testArticleFlags() {
+ $op = $this->newInstance();
+ $this->assertFalse( $op->isArticle() );
+ $this->assertTrue( $op->isArticleRelated() );
+
+ $op->setArticleRelated( false );
+ $this->assertFalse( $op->isArticle() );
+ $this->assertFalse( $op->isArticleRelated() );
+
+ $op->setArticleFlag( true );
+ $this->assertTrue( $op->isArticle() );
+ $this->assertTrue( $op->isArticleRelated() );
+
+ $op->setArticleFlag( false );
+ $this->assertFalse( $op->isArticle() );
+ $this->assertTrue( $op->isArticleRelated() );
+
+ $op->setArticleFlag( true );
+ $op->setArticleRelated( false );
+ $this->assertFalse( $op->isArticle() );
+ $this->assertFalse( $op->isArticleRelated() );
+ }
+
+ /**
+ * @covers OutputPage::addLanguageLinks
+ * @covers OutputPage::setLanguageLinks
+ * @covers OutputPage::getLanguageLinks
+ * @covers OutputPage::addParserOutputMetadata
+ */
+ function testLanguageLinks() {
+ $op = $this->newInstance();
+ $this->assertSame( [], $op->getLanguageLinks() );
+
+ $op->addLanguageLinks( [ 'fr:A', 'it:B' ] );
+ $this->assertSame( [ 'fr:A', 'it:B' ], $op->getLanguageLinks() );
+
+ $op->addLanguageLinks( [ 'de:C', 'es:D' ] );
+ $this->assertSame( [ 'fr:A', 'it:B', 'de:C', 'es:D' ], $op->getLanguageLinks() );
+
+ $op->setLanguageLinks( [ 'pt:E' ] );
+ $this->assertSame( [ 'pt:E' ], $op->getLanguageLinks() );
+
+ $po = new ParserOutput();
+ $po->setLanguageLinks( [ 'he:F', 'ar:G' ] );
+ $op->addParserOutputMetadata( $po );
+ $this->assertSame( [ 'pt:E', 'he:F', 'ar:G' ], $op->getLanguageLinks() );
+ }
+
+ // @todo Are these category links tests too abstract and complicated for what they test? Would
+ // it make sense to just write out all the tests by hand with maybe some copy-and-paste?
+
+ /**
+ * @dataProvider provideGetCategories
+ *
* @covers OutputPage::addCategoryLinks
* @covers OutputPage::getCategories
+ * @covers OutputPage::getCategoryLinks
+ *
+ * @param array $args Array of form [ category name => sort key ]
+ * @param array $fakeResults Array of form [ category name => value to return from mocked
+ * LinkBatch ]
+ * @param callback $variantLinkCallback Callback to replace findVariantLink() call
+ * @param array $expectedNormal Expected return value of getCategoryLinks['normal']
+ * @param array $expectedHidden Expected return value of getCategoryLinks['hidden']
*/
- public function testGetCategories() {
- $fakeResultWrapper = new FakeResultWrapper( [
- (object)[
- 'pp_value' => 1,
- 'page_title' => 'Test'
- ],
- (object)[
- 'page_title' => 'Test2'
- ]
- ] );
+ public function testAddCategoryLinks(
+ array $args, array $fakeResults, callable $variantLinkCallback = null,
+ array $expectedNormal, array $expectedHidden
+ ) {
+ $expectedNormal = $this->extractExpectedCategories( $expectedNormal, 'add' );
+ $expectedHidden = $this->extractExpectedCategories( $expectedHidden, 'add' );
+
+ $op = $this->setupCategoryTests( $fakeResults, $variantLinkCallback );
+
+ $op->addCategoryLinks( $args );
+
+ $this->doCategoryAsserts( $op, $expectedNormal, $expectedHidden );
+ $this->doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden );
+ }
+
+ /**
+ * @dataProvider provideGetCategories
+ *
+ * @covers OutputPage::addCategoryLinks
+ * @covers OutputPage::getCategories
+ * @covers OutputPage::getCategoryLinks
+ */
+ public function testAddCategoryLinksOneByOne(
+ array $args, array $fakeResults, callable $variantLinkCallback = null,
+ array $expectedNormal, array $expectedHidden
+ ) {
+ if ( count( $args ) <= 1 ) {
+ // @todo Should this be skipped instead of passed?
+ $this->assertTrue( true );
+ return;
+ }
+
+ $expectedNormal = $this->extractExpectedCategories( $expectedNormal, 'onebyone' );
+ $expectedHidden = $this->extractExpectedCategories( $expectedHidden, 'onebyone' );
+
+ $op = $this->setupCategoryTests( $fakeResults, $variantLinkCallback );
+
+ foreach ( $args as $key => $val ) {
+ $op->addCategoryLinks( [ $key => $val ] );
+ }
+
+ $this->doCategoryAsserts( $op, $expectedNormal, $expectedHidden );
+ $this->doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden );
+ }
+
+ /**
+ * @dataProvider provideGetCategories
+ *
+ * @covers OutputPage::setCategoryLinks
+ * @covers OutputPage::getCategories
+ * @covers OutputPage::getCategoryLinks
+ */
+ public function testSetCategoryLinks(
+ array $args, array $fakeResults, callable $variantLinkCallback = null,
+ array $expectedNormal, array $expectedHidden
+ ) {
+ $expectedNormal = $this->extractExpectedCategories( $expectedNormal, 'set' );
+ $expectedHidden = $this->extractExpectedCategories( $expectedHidden, 'set' );
+
+ $op = $this->setupCategoryTests( $fakeResults, $variantLinkCallback );
+
+ $op->setCategoryLinks( [ 'Initial page' => 'Initial page' ] );
+ $op->setCategoryLinks( $args );
+
+ // We don't reset the categories, for some reason, only the links
+ $expectedNormalCats = array_merge( [ 'Initial page' ], $expectedNormal );
+ $expectedCats = array_merge( $expectedHidden, $expectedNormalCats );
+
+ $this->doCategoryAsserts( $op, $expectedNormalCats, $expectedHidden );
+ $this->doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden );
+ }
+
+ /**
+ * @dataProvider provideGetCategories
+ *
+ * @covers OutputPage::addParserOutputMetadata
+ * @covers OutputPage::getCategories
+ * @covers OutputPage::getCategoryLinks
+ */
+ public function testParserOutputCategoryLinks(
+ array $args, array $fakeResults, callable $variantLinkCallback = null,
+ array $expectedNormal, array $expectedHidden
+ ) {
+ $expectedNormal = $this->extractExpectedCategories( $expectedNormal, 'pout' );
+ $expectedHidden = $this->extractExpectedCategories( $expectedHidden, 'pout' );
+
+ $op = $this->setupCategoryTests( $fakeResults, $variantLinkCallback );
+
+ $stubPO = $this->createParserOutputStub( 'getCategories', $args );
+
+ $op->addParserOutputMetadata( $stubPO );
+
+ $this->doCategoryAsserts( $op, $expectedNormal, $expectedHidden );
+ $this->doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden );
+ }
+
+ /**
+ * We allow different expectations for different tests as an associative array, like
+ * [ 'set' => [ ... ], 'default' => [ ... ] ] if setCategoryLinks() will give a different
+ * result.
+ */
+ private function extractExpectedCategories( array $expected, $key ) {
+ if ( !$expected || isset( $expected[0] ) ) {
+ return $expected;
+ }
+ return $expected[$key] ?? $expected['default'];
+ }
+
+ private function setupCategoryTests(
+ array $fakeResults, callable $variantLinkCallback = null
+ ) : OutputPage {
+ $this->setMwGlobals( 'wgUsePigLatinVariant', true );
+
$op = $this->getMockBuilder( OutputPage::class )
->setConstructorArgs( [ new RequestContext() ] )
->setMethods( [ 'addCategoryLinksToLBAndGetResult' ] )
->getMock();
+
$op->expects( $this->any() )
->method( 'addCategoryLinksToLBAndGetResult' )
- ->will( $this->returnValue( $fakeResultWrapper ) );
+ ->will( $this->returnCallback( function ( array $categories ) use ( $fakeResults ) {
+ $return = [];
+ foreach ( $categories as $category => $unused ) {
+ if ( isset( $fakeResults[$category] ) ) {
+ $return[] = $fakeResults[$category];
+ }
+ }
+ return new FakeResultWrapper( $return );
+ } ) );
+
+ if ( $variantLinkCallback ) {
+ $mockContLang = $this->getMockBuilder( Language::class )
+ ->setConstructorArgs( [ 'en' ] )
+ ->setMethods( [ 'findVariantLink' ] )
+ ->getMock();
+ $mockContLang->expects( $this->any() )
+ ->method( 'findVariantLink' )
+ ->will( $this->returnCallback( $variantLinkCallback ) );
+ $this->setContentLang( $mockContLang );
+ }
- $op->addCategoryLinks( [
- 'Test' => 'Test',
- 'Test2' => 'Test2',
+ $this->assertSame( [], $op->getCategories() );
+
+ return $op;
+ }
+
+ private function doCategoryAsserts( $op, $expectedNormal, $expectedHidden ) {
+ $this->assertSame( array_merge( $expectedHidden, $expectedNormal ), $op->getCategories() );
+ $this->assertSame( $expectedNormal, $op->getCategories( 'normal' ) );
+ $this->assertSame( $expectedHidden, $op->getCategories( 'hidden' ) );
+ }
+
+ private function doCategoryLinkAsserts( $op, $expectedNormal, $expectedHidden ) {
+ $catLinks = $op->getCategoryLinks();
+ $this->assertSame( (bool)$expectedNormal + (bool)$expectedHidden, count( $catLinks ) );
+ if ( $expectedNormal ) {
+ $this->assertSame( count( $expectedNormal ), count( $catLinks['normal'] ) );
+ }
+ if ( $expectedHidden ) {
+ $this->assertSame( count( $expectedHidden ), count( $catLinks['hidden'] ) );
+ }
+
+ foreach ( $expectedNormal as $i => $name ) {
+ $this->assertContains( $name, $catLinks['normal'][$i] );
+ }
+ foreach ( $expectedHidden as $i => $name ) {
+ $this->assertContains( $name, $catLinks['hidden'][$i] );
+ }
+ }
+
+ public function provideGetCategories() {
+ return [
+ 'No categories' => [ [], [], null, [], [] ],
+ 'Simple test' => [
+ [ 'Test1' => 'Some sortkey', 'Test2' => 'A different sortkey' ],
+ [ 'Test1' => (object)[ 'pp_value' => 1, 'page_title' => 'Test1' ],
+ 'Test2' => (object)[ 'page_title' => 'Test2' ] ],
+ null,
+ [ 'Test2' ],
+ [ 'Test1' ],
+ ],
+ 'Invalid title' => [
+ [ '[' => '[', 'Test' => 'Test' ],
+ [ 'Test' => (object)[ 'page_title' => 'Test' ] ],
+ null,
+ [ 'Test' ],
+ [],
+ ],
+ 'Variant link' => [
+ [ 'Test' => 'Test', 'Estay' => 'Estay' ],
+ [ 'Test' => (object)[ 'page_title' => 'Test' ] ],
+ function ( &$link, &$title ) {
+ if ( $link === 'Estay' ) {
+ $link = 'Test';
+ $title = Title::makeTitleSafe( NS_CATEGORY, $link );
+ }
+ },
+ // For adding one by one, the variant gets added as well as the original category,
+ // but if you add them all together the second time gets skipped.
+ [ 'onebyone' => [ 'Test', 'Test' ], 'default' => [ 'Test' ] ],
+ [],
+ ],
+ ];
+ }
+
+ /**
+ * @covers OutputPage::getCategories
+ */
+ public function testGetCategoriesInvalid() {
+ $this->setExpectedException( InvalidArgumentException::class,
+ 'Invalid category type given: hiddne' );
+
+ $op = $this->newInstance();
+ $op->getCategories( 'hiddne' );
+ }
+
+ // @todo Should we test addCategoryLinksToLBAndGetResult? If so, how? Insert some test rows in
+ // the DB?
+
+ /**
+ * @covers OutputPage::setIndicators
+ * @covers OutputPage::getIndicators
+ * @covers OutputPage::addParserOutputMetadata
+ */
+ public function testIndicators() {
+ $op = $this->newInstance();
+ $this->assertSame( [], $op->getIndicators() );
+
+ $op->setIndicators( [] );
+ $this->assertSame( [], $op->getIndicators() );
+
+ // Test sorting alphabetically
+ $op->setIndicators( [ 'b' => 'x', 'a' => 'y' ] );
+ $this->assertSame( [ 'a' => 'y', 'b' => 'x' ], $op->getIndicators() );
+
+ // Test overwriting existing keys
+ $op->setIndicators( [ 'c' => 'z', 'a' => 'w' ] );
+ $this->assertSame( [ 'a' => 'w', 'b' => 'x', 'c' => 'z' ], $op->getIndicators() );
+
+ // Test with ParserOutput
+ $stubPO = $this->createParserOutputStub( 'getIndicators', [ 'c' => 'u', 'd' => 'v' ] );
+ $op->addParserOutputMetadata( $stubPO );
+ $this->assertSame( [ 'a' => 'w', 'b' => 'x', 'c' => 'u', 'd' => 'v' ],
+ $op->getIndicators() );
+ }
+
+ /**
+ * @covers OutputPage::addHelpLink
+ * @covers OutputPage::getIndicators
+ */
+ public function testAddHelpLink() {
+ $op = $this->newInstance();
+
+ $op->addHelpLink( 'Manual:PHP unit testing' );
+ $indicators = $op->getIndicators();
+ $this->assertSame( [ 'mw-helplink' ], array_keys( $indicators ) );
+ $this->assertContains( 'Manual:PHP_unit_testing', $indicators['mw-helplink'] );
+
+ $op->addHelpLink( 'https://phpunit.de', true );
+ $indicators = $op->getIndicators();
+ $this->assertSame( [ 'mw-helplink' ], array_keys( $indicators ) );
+ $this->assertContains( 'https://phpunit.de', $indicators['mw-helplink'] );
+ $this->assertNotContains( 'mediawiki', $indicators['mw-helplink'] );
+ $this->assertNotContains( 'Manual:PHP', $indicators['mw-helplink'] );
+ }
+
+ /**
+ * @covers OutputPage::prependHTML
+ * @covers OutputPage::addHTML
+ * @covers OutputPage::addElement
+ * @covers OutputPage::clearHTML
+ * @covers OutputPage::getHTML
+ */
+ public function testBodyHTML() {
+ $op = $this->newInstance();
+ $this->assertSame( '', $op->getHTML() );
+
+ $op->addHTML( 'a' );
+ $this->assertSame( 'a', $op->getHTML() );
+
+ $op->addHTML( 'b' );
+ $this->assertSame( 'ab', $op->getHTML() );
+
+ $op->prependHTML( 'c' );
+ $this->assertSame( 'cab', $op->getHTML() );
+
+ $op->addElement( 'p', [ 'id' => 'foo' ], 'd' );
+ $this->assertSame( 'cab<p id="foo">d</p>', $op->getHTML() );
+
+ $op->clearHTML();
+ $this->assertSame( '', $op->getHTML() );
+ }
+
+ /**
+ * @dataProvider provideRevisionId
+ * @covers OutputPage::setRevisionId
+ * @covers OutputPage::getRevisionId
+ */
+ public function testRevisionId( $newVal, $expected ) {
+ $op = $this->newInstance();
+
+ $this->assertNull( $op->setRevisionId( $newVal ) );
+ $this->assertSame( $expected, $op->getRevisionId() );
+ $this->assertSame( $expected, $op->setRevisionId( null ) );
+ $this->assertNull( $op->getRevisionId() );
+ }
+
+ public function provideRevisionId() {
+ return [
+ [ null, null ],
+ [ 7, 7 ],
+ [ -1, -1 ],
+ [ 3.2, 3 ],
+ [ '0', 0 ],
+ [ '32% finished', 32 ],
+ [ false, 0 ],
+ ];
+ }
+
+ /**
+ * @covers OutputPage::setRevisionTimestamp
+ * @covers OutputPage::getRevisionTimestamp
+ */
+ public function testRevisionTimestamp() {
+ $op = $this->newInstance();
+ $this->assertNull( $op->getRevisionTimestamp() );
+
+ $this->assertNull( $op->setRevisionTimestamp( 'abc' ) );
+ $this->assertSame( 'abc', $op->getRevisionTimestamp() );
+ $this->assertSame( 'abc', $op->setRevisionTimestamp( null ) );
+ $this->assertNull( $op->getRevisionTimestamp() );
+ }
+
+ /**
+ * @covers OutputPage::setFileVersion
+ * @covers OutputPage::getFileVersion
+ */
+ public function testFileVersion() {
+ $op = $this->newInstance();
+ $this->assertNull( $op->getFileVersion() );
+
+ $stubFile = $this->createMock( File::class );
+ $stubFile->method( 'exists' )->willReturn( true );
+ $stubFile->method( 'getTimestamp' )->willReturn( '12211221123321' );
+ $stubFile->method( 'getSha1' )->willReturn( 'bf3ffa7047dc080f5855377a4f83cd18887e3b05' );
+
+ $op->setFileVersion( $stubFile );
+
+ $this->assertEquals(
+ [ 'time' => '12211221123321', 'sha1' => 'bf3ffa7047dc080f5855377a4f83cd18887e3b05' ],
+ $op->getFileVersion()
+ );
+
+ $stubMissingFile = $this->createMock( File::class );
+ $stubMissingFile->method( 'exists' )->willReturn( false );
+
+ $op->setFileVersion( $stubMissingFile );
+ $this->assertNull( $op->getFileVersion() );
+
+ $op->setFileVersion( $stubFile );
+ $this->assertNotNull( $op->getFileVersion() );
+
+ $op->setFileVersion( null );
+ $this->assertNull( $op->getFileVersion() );
+ }
+
+ private function createParserOutputStub( $method = '', $retVal = [] ) {
+ $pOut = $this->getMock( ParserOutput::class );
+ if ( $method !== '' ) {
+ $pOut->method( $method )->willReturn( $retVal );
+ }
+
+ $arrayReturningMethods = [
+ 'getCategories',
+ 'getFileSearchOptions',
+ 'getHeadItems',
+ 'getIndicators',
+ 'getLanguageLinks',
+ 'getOutputHooks',
+ 'getTemplateIds',
+ ];
+
+ foreach ( $arrayReturningMethods as $method ) {
+ $pOut->method( $method )->willReturn( [] );
+ }
+
+ return $pOut;
+ }
+
+ /**
+ * @covers OutputPage::getTemplateIds
+ * @covers OutputPage::addParserOutputMetadata
+ */
+ public function testTemplateIds() {
+ $op = $this->newInstance();
+ $this->assertSame( [], $op->getTemplateIds() );
+
+ // Test with no template id's
+ $stubPOEmpty = $this->createParserOutputStub();
+ $op->addParserOutputMetadata( $stubPOEmpty );
+ $this->assertSame( [], $op->getTemplateIds() );
+
+ // Test with some arbitrary template id's
+ $ids = [
+ NS_MAIN => [ 'A' => 3, 'B' => 17 ],
+ NS_TALK => [ 'C' => 31 ],
+ NS_MEDIA => [ 'D' => -1 ],
+ ];
+
+ $stubPO1 = $this->createParserOutputStub( 'getTemplateIds', $ids );
+
+ $op->addParserOutputMetadata( $stubPO1 );
+ $this->assertSame( $ids, $op->getTemplateIds() );
+
+ // Test merging with a second set of id's
+ $stubPO2 = $this->createParserOutputStub( 'getTemplateIds', [
+ NS_MAIN => [ 'E' => 1234 ],
+ NS_PROJECT => [ 'F' => 5678 ],
] );
- $this->assertEquals( [ 0 => 'Test', '1' => 'Test2' ], $op->getCategories() );
- $this->assertEquals( [ 0 => 'Test2' ], $op->getCategories( 'normal' ) );
- $this->assertEquals( [ 0 => 'Test' ], $op->getCategories( 'hidden' ) );
+
+ $finalIds = [
+ NS_MAIN => [ 'E' => 1234, 'A' => 3, 'B' => 17 ],
+ NS_TALK => [ 'C' => 31 ],
+ NS_MEDIA => [ 'D' => -1 ],
+ NS_PROJECT => [ 'F' => 5678 ],
+ ];
+
+ $op->addParserOutputMetadata( $stubPO2 );
+ $this->assertSame( $finalIds, $op->getTemplateIds() );
+
+ // Test merging with an empty set of id's
+ $op->addParserOutputMetadata( $stubPOEmpty );
+ $this->assertSame( $finalIds, $op->getTemplateIds() );
+ }
+
+ /**
+ * @covers OutputPage::getFileSearchOptions
+ * @covers OutputPage::addParserOutputMetadata
+ */
+ public function testFileSearchOptions() {
+ $op = $this->newInstance();
+ $this->assertSame( [], $op->getFileSearchOptions() );
+
+ // Test with no files
+ $stubPOEmpty = $this->createParserOutputStub();
+
+ $op->addParserOutputMetadata( $stubPOEmpty );
+ $this->assertSame( [], $op->getFileSearchOptions() );
+
+ // Test with some arbitrary files
+ $files1 = [
+ 'A' => [ 'time' => null, 'sha1' => '' ],
+ 'B' => [
+ 'time' => '12211221123321',
+ 'sha1' => 'bf3ffa7047dc080f5855377a4f83cd18887e3b05',
+ ],
+ ];
+
+ $stubPO1 = $this->createParserOutputStub( 'getFileSearchOptions', $files1 );
+
+ $op->addParserOutputMetadata( $stubPO1 );
+ $this->assertSame( $files1, $op->getFileSearchOptions() );
+
+ // Test merging with a second set of files
+ $files2 = [
+ 'C' => [ 'time' => null, 'sha1' => '' ],
+ 'B' => [ 'time' => null, 'sha1' => '' ],
+ ];
+
+ $stubPO2 = $this->createParserOutputStub( 'getFileSearchOptions', $files2 );
+
+ $op->addParserOutputMetadata( $stubPO2 );
+ $this->assertSame( array_merge( $files1, $files2 ), $op->getFileSearchOptions() );
+
+ // Test merging with an empty set of files
+ $op->addParserOutputMetadata( $stubPOEmpty );
+ $this->assertSame( array_merge( $files1, $files2 ), $op->getFileSearchOptions() );
}
+ /**
+ * @dataProvider provideAddWikiText
+ * @covers OutputPage::addWikiText
+ * @covers OutputPage::addWikiTextWithTitle
+ * @covers OutputPage::addWikiTextTitle
+ * @covers OutputPage::getHTML
+ */
+ public function testAddWikiText( $method, array $args, $expected ) {
+ $op = $this->newInstance();
+ $this->assertSame( '', $op->getHTML() );
+
+ if ( in_array(
+ $method,
+ [ 'addWikiTextWithTitle', 'addWikiTextTitleTidy', 'addWikiTextTitle' ]
+ ) && count( $args ) >= 2 && $args[1] === null ) {
+ // Special placeholder because we can't get the actual title in the provider
+ $args[1] = $op->getTitle();
+ }
+
+ $op->$method( ...$args );
+ $this->assertSame( $expected, $op->getHTML() );
+ }
+
+ public function provideAddWikiText() {
+ $tests = [
+ 'addWikiText' => [
+ 'Simple wikitext' => [
+ [ "'''Bold'''" ],
+ "<p><b>Bold</b>\n</p>",
+ ], 'List at start' => [
+ [ '* List' ],
+ "<ul><li>List</li></ul>\n",
+ ], 'List not at start' => [
+ [ '* Not a list', false ],
+ '* Not a list',
+ ], 'Non-interface' => [
+ [ "'''Bold'''", true, false ],
+ "<div class=\"mw-parser-output\"><p><b>Bold</b>\n</p></div>",
+ ], 'No section edit links' => [
+ [ '== Title ==' ],
+ "<h2><span class=\"mw-headline\" id=\"Title\">Title</span></h2>\n",
+ ],
+ ],
+ 'addWikiTextWithTitle' => [
+ 'With title at start' => [
+ [ '* {{PAGENAME}}', Title::newFromText( 'Talk:Some page' ) ],
+ "<div class=\"mw-parser-output\"><ul><li>Some page</li></ul>\n</div>",
+ ], 'With title at start' => [
+ [ '* {{PAGENAME}}', Title::newFromText( 'Talk:Some page' ), false ],
+ "<div class=\"mw-parser-output\">* Some page</div>",
+ ],
+ ],
+ ];
+
+ // Test all the others on addWikiTextTitle as well
+ foreach ( $tests['addWikiText'] as $key => $val ) {
+ $args = [ $val[0][0], null, $val[0][1] ?? true, false, $val[0][2] ?? true ];
+ $tests['addWikiTextTitle']["$key (addWikiTextTitle)"] =
+ array_merge( [ $args ], array_slice( $val, 1 ) );
+ }
+ foreach ( $tests['addWikiTextWithTitle'] as $key => $val ) {
+ $args = [ $val[0][0], $val[0][1], $val[0][2] ?? true ];
+ $tests['addWikiTextTitle']["$key (addWikiTextTitle)"] =
+ array_merge( [ $args ], array_slice( $val, 1 ) );
+ }
+
+ // We have to reformat our array to match what PHPUnit wants
+ $ret = [];
+ foreach ( $tests as $key => $subarray ) {
+ foreach ( $subarray as $subkey => $val ) {
+ $val = array_merge( [ $key ], $val );
+ $ret[$subkey] = $val;
+ }
+ }
+
+ return $ret;
+ }
+
+ /**
+ * @covers OutputPage::addWikiText
+ */
+ public function testAddWikiTextNoTitle() {
+ $this->setExpectedException( MWException::class, 'Title is null' );
+
+ $op = $this->newInstance( [], null, 'notitle' );
+ $op->addWikiText( 'a' );
+ }
+
+ // @todo How should we cover the Tidy variants?
+
+ /**
+ * @covers OutputPage::addParserOutputMetadata
+ */
+ public function testNoGallery() {
+ $op = $this->newInstance();
+ $this->assertFalse( $op->mNoGallery );
+
+ $stubPO1 = $this->createParserOutputStub( 'getNoGallery', true );
+ $op->addParserOutputMetadata( $stubPO1 );
+ $this->assertTrue( $op->mNoGallery );
+
+ $stubPO2 = $this->createParserOutputStub( 'getNoGallery', false );
+ $op->addParserOutputMetadata( $stubPO2 );
+ $this->assertFalse( $op->mNoGallery );
+ }
+
+ // @todo Make sure to test the following in addParserOutputMetadata() as well when we add tests
+ // for them:
+ // * enableClientCache()
+ // * addModules()
+ // * addModuleScripts()
+ // * addModuleStyles()
+ // * addJsConfigVars()
+ // * preventClickJacking()
+ // Otherwise those lines of addParserOutputMetadata() will be reported as covered, but we won't
+ // be testing they actually work.
+
/**
* @covers OutputPage::haveCacheVaryCookies
*/
/**
* @return OutputPage
*/
- private function newInstance( $config = [], WebRequest $request = null ) {
+ private function newInstance( $config = [], WebRequest $request = null, $options = [] ) {
$context = new RequestContext();
$context->setConfig( new MultiConfig( [
$context->getConfig()
] ) );
- $context->setTitle( Title::newFromText( 'My test page' ) );
+ if ( !in_array( 'notitle', (array)$options ) ) {
+ $context->setTitle( Title::newFromText( 'My test page' ) );
+ }
if ( $request ) {
$context->setRequest( $request );
private $the_properties;
protected function setUp() {
- global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
-
parent::setUp();
- $wgExtraNamespaces[12312] = 'Dummy';
- $wgExtraNamespaces[12313] = 'Dummy_talk';
-
- $wgNamespaceContentModels[12312] = 'DUMMY';
- $wgContentHandlers['DUMMY'] = 'DummyContentHandlerForTesting';
+ $this->setMwGlobals( [
+ 'wgExtraNamespaces' => [
+ 12312 => 'Dummy',
+ 12313 => 'Dummy_talk',
+ ],
+ 'wgNamespaceContentModels' => [ 12312 => 'DUMMY' ],
+ ] );
- MWNamespace::clearCaches();
- $wgContLang->resetNamespaces(); # reset namespace cache
+ $this->mergeMwGlobalArrayValue(
+ 'wgContentHandlers',
+ [ 'DUMMY' => 'DummyContentHandlerForTesting' ]
+ );
if ( !$this->the_properties ) {
$this->the_properties = [
}
}
- protected function tearDown() {
- global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
-
- parent::tearDown();
-
- unset( $wgExtraNamespaces[12312] );
- unset( $wgExtraNamespaces[12313] );
-
- unset( $wgNamespaceContentModels[12312] );
- unset( $wgContentHandlers['DUMMY'] );
-
- MWNamespace::clearCaches();
- $wgContLang->resetNamespaces(); # reset namespace cache
- }
-
/**
* Test getting a single property from a single page. The property was
* set in setUp().
$this->markTestSkipped( 'Main namespace does not support wikitext.' );
}
- // Avoid special pages from extensions interferring with the tests
+ // Avoid special pages from extensions interfering with the tests
$this->setMwGlobals( [
'wgSpecialPages' => [],
'wgHooks' => [],
$this->originalHandlers = TestingAccessWrapper::newFromClass( Hooks::class )->handlers;
TestingAccessWrapper::newFromClass( Hooks::class )->handlers = [];
- // Clear caches so that our new namespace appears
- MWNamespace::clearCaches();
- Language::factory( 'en' )->resetNamespaces();
-
SpecialPageFactory::resetList();
}
public function tearDown() {
- MWNamespace::clearCaches();
- Language::factory( 'en' )->resetNamespaces();
-
parent::tearDown();
TestingAccessWrapper::newFromClass( Hooks::class )->handlers = $this->originalHandlers;
abstract protected function getMcrTablesToReset();
protected function setUp() {
- global $wgContLang;
-
$this->tablesUsed += $this->getMcrTablesToReset();
parent::setUp();
$this->getMcrMigrationStage()
);
- MWNamespace::clearCaches();
- // Reset namespace cache
- $wgContLang->resetNamespaces();
-
$this->overrideMwServices();
if ( !$this->testPage ) {
}
}
- protected function tearDown() {
- global $wgContLang;
-
- parent::tearDown();
-
- MWNamespace::clearCaches();
- // Reset namespace cache
- $wgContLang->resetNamespaces();
- }
-
abstract protected function getContentHandlerUseDB();
private function makeRevisionWithProps( $props = null ) {
<?php
+use MediaWiki\MediaWikiServices;
+
/**
* Wraps the user object, so we can also retain full access to properties
* like password if we log in via the API.
throw new MWException( "Passed User has an ID but is not in the database?" );
}
- $passwordFactory = new PasswordFactory();
- $passwordFactory->init( RequestContext::getMain()->getConfig() );
+ $passwordFactory = MediaWikiServices::getInstance()->getPasswordFactory();
if ( !$passwordFactory->newFromCiphertext( $row->user_password )->equals( $password ) ) {
$passwordHash = $passwordFactory->newFromPlaintext( $password );
$dbw->update(
class TitleMethodsTest extends MediaWikiLangTestCase {
protected function setUp() {
- global $wgContLang;
-
parent::setUp();
$this->mergeMwGlobalArrayValue(
12302 => CONTENT_MODEL_JAVASCRIPT,
]
);
-
- MWNamespace::clearCaches();
- $wgContLang->resetNamespaces(); # reset namespace cache
- }
-
- protected function tearDown() {
- global $wgContLang;
-
- parent::tearDown();
-
- MWNamespace::clearCaches();
- $wgContLang->resetNamespaces(); # reset namespace cache
}
public static function provideEquals() {
class ApiEditPageTest extends ApiTestCase {
protected function setUp() {
- global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
-
parent::setUp();
- $this->setContentLang( $wgContLang );
-
$this->setMwGlobals( [
- 'wgExtraNamespaces' => $wgExtraNamespaces,
- 'wgNamespaceContentModels' => $wgNamespaceContentModels,
- 'wgContentHandlers' => $wgContentHandlers,
+ 'wgExtraNamespaces' => [
+ 12312 => 'Dummy',
+ 12313 => 'Dummy_talk',
+ 12314 => 'DummyNonText',
+ 12315 => 'DummyNonText_talk',
+ ],
+ 'wgNamespaceContentModels' => [
+ 12312 => 'testing',
+ 12314 => 'testing-nontext',
+ ],
+ ] );
+ $this->mergeMwGlobalArrayValue( 'wgContentHandlers', [
+ 'testing' => 'DummyContentHandlerForTesting',
+ 'testing-nontext' => 'DummyNonTextContentHandler',
+ 'testing-serialize-error' => 'DummySerializeErrorContentHandler',
] );
-
- $wgExtraNamespaces[12312] = 'Dummy';
- $wgExtraNamespaces[12313] = 'Dummy_talk';
- $wgExtraNamespaces[12314] = 'DummyNonText';
- $wgExtraNamespaces[12315] = 'DummyNonText_talk';
-
- $wgNamespaceContentModels[12312] = "testing";
- $wgNamespaceContentModels[12314] = "testing-nontext";
-
- $wgContentHandlers["testing"] = 'DummyContentHandlerForTesting';
- $wgContentHandlers["testing-nontext"] = 'DummyNonTextContentHandler';
- $wgContentHandlers["testing-serialize-error"] =
- 'DummySerializeErrorContentHandler';
-
- MWNamespace::clearCaches();
- $wgContLang->resetNamespaces(); # reset namespace cache
- }
-
- protected function tearDown() {
- global $wgContLang;
-
- MWNamespace::clearCaches();
- $wgContLang->resetNamespaces(); # reset namespace cache
-
- parent::tearDown();
}
public function testEdit() {
<?php
+use MediaWiki\MediaWikiServices;
use Wikimedia\TestingAccessWrapper;
/**
$this->assertNotEquals( 0, $centralId, 'sanity check' );
$password = 'ngfhmjm64hv0854493hsj5nncjud2clk';
- $passwordFactory = new PasswordFactory();
- $passwordFactory->init( RequestContext::getMain()->getConfig() );
+ $passwordFactory = MediaWikiServices::getInstance()->getPasswordFactory();
// A is unsalted MD5 (thus fast) ... we don't care about security here, this is test only
$passwordHash = $passwordFactory->newFromPlaintext( $password );
--- /dev/null
+<?php
+
+/**
+ * @group API
+ * @group medium
+ *
+ * @covers ApiQuerySiteinfo
+ */
+class ApiQuerySiteinfoTest extends ApiTestCase {
+ // We don't try to test every single thing for every category, just a sample
+
+ protected function doQuery( $siprop = null, $extraParams = [] ) {
+ $params = [ 'action' => 'query', 'meta' => 'siteinfo' ];
+ if ( $siprop !== null ) {
+ $params['siprop'] = $siprop;
+ }
+ $params = array_merge( $params, $extraParams );
+
+ $res = $this->doApiRequest( $params );
+
+ $this->assertArrayNotHasKey( 'warnings', $res[0] );
+ $this->assertCount( 1, $res[0]['query'] );
+
+ return $res[0]['query'][$siprop === null ? 'general' : $siprop];
+ }
+
+ public function testGeneral() {
+ $this->setMwGlobals( [
+ 'wgAllowExternalImagesFrom' => '//localhost/',
+ ] );
+
+ $data = $this->doQuery();
+
+ $this->assertSame( Title::newMainPage()->getPrefixedText(), $data['mainpage'] );
+ $this->assertSame( PHP_VERSION, $data['phpversion'] );
+ $this->assertSame( [ '//localhost/' ], $data['externalimages'] );
+ }
+
+ public function testLinkPrefixCharset() {
+ global $wgContLang;
+
+ $this->setContentLang( 'ar' );
+ $this->assertTrue( $wgContLang->linkPrefixExtension(), 'Sanity check' );
+
+ $data = $this->doQuery();
+
+ $this->assertSame( $wgContLang->linkPrefixCharset(), $data['linkprefixcharset'] );
+ }
+
+ public function testVariants() {
+ global $wgContLang;
+
+ $this->setContentLang( 'zh' );
+ $this->assertTrue( $wgContLang->hasVariants(), 'Sanity check' );
+
+ $data = $this->doQuery();
+
+ $expected = array_map(
+ function ( $code ) use ( $wgContLang ) {
+ return [ 'code' => $code, 'name' => $wgContLang->getVariantname( $code ) ];
+ },
+ $wgContLang->getVariants()
+ );
+
+ $this->assertSame( $expected, $data['variants'] );
+ }
+
+ public function testReadOnly() {
+ $svc = \MediaWiki\MediaWikiServices::getInstance()->getReadOnlyMode();
+ $svc->setReason( 'Need more donations' );
+ try {
+ $data = $this->doQuery();
+ } finally {
+ $svc->setReason( false );
+ }
+
+ $this->assertTrue( $data['readonly'] );
+ $this->assertSame( 'Need more donations', $data['readonlyreason'] );
+ }
+
+ public function testNamespaces() {
+ global $wgContLang;
+
+ $this->setMwGlobals( 'wgExtraNamespaces', [ '138' => 'Testing' ] );
+
+ $this->assertSame( array_keys( $wgContLang->getFormattedNamespaces() ),
+ array_keys( $this->doQuery( 'namespaces' ) ) );
+ }
+
+ public function testNamespaceAliases() {
+ global $wgNamespaceAliases, $wgContLang;
+
+ $expected = array_merge( $wgNamespaceAliases, $wgContLang->getNamespaceAliases() );
+ $expected = array_map(
+ function ( $key, $val ) {
+ return [ 'id' => $val, 'alias' => strtr( $key, '_', ' ' ) ];
+ },
+ array_keys( $expected ),
+ $expected
+ );
+
+ // Test that we don't list duplicates
+ $this->mergeMwGlobalArrayValue( 'wgNamespaceAliases', [ 'Talk' => NS_TALK ] );
+
+ $this->assertSame( $expected, $this->doQuery( 'namespacealiases' ) );
+ }
+
+ public function testSpecialPageAliases() {
+ $this->assertCount(
+ count( SpecialPageFactory::getNames() ),
+ $this->doQuery( 'specialpagealiases' )
+ );
+ }
+
+ public function testMagicWords() {
+ global $wgContLang;
+
+ $this->assertCount(
+ count( $wgContLang->getMagicWords() ),
+ $this->doQuery( 'magicwords' )
+ );
+ }
+
+ /**
+ * @dataProvider interwikiMapProvider
+ */
+ public function testInterwikiMap( $filter ) {
+ global $wgServer, $wgScriptPath;
+
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->insert(
+ 'interwiki',
+ [
+ [
+ 'iw_prefix' => 'self',
+ 'iw_url' => "$wgServer$wgScriptPath/index.php?title=$1",
+ 'iw_api' => "$wgServer$wgScriptPath/api.php",
+ 'iw_wikiid' => 'somedbname',
+ 'iw_local' => true,
+ 'iw_trans' => true,
+ ],
+ [
+ 'iw_prefix' => 'foreign',
+ 'iw_url' => '//foreign.example/wiki/$1',
+ 'iw_api' => '',
+ 'iw_wikiid' => '',
+ 'iw_local' => false,
+ 'iw_trans' => false,
+ ],
+ ],
+ __METHOD__,
+ 'IGNORE'
+ );
+ $this->tablesUsed[] = 'interwiki';
+
+ $this->setMwGlobals( [
+ 'wgLocalInterwikis' => [ 'self' ],
+ 'wgExtraInterlanguageLinkPrefixes' => [ 'self' ],
+ 'wgExtraLanguageNames' => [ 'self' => 'Recursion' ],
+ ] );
+
+ MessageCache::singleton()->enable();
+
+ $this->editPage( 'MediaWiki:Interlanguage-link-self', 'Self!' );
+ $this->editPage( 'MediaWiki:Interlanguage-link-sitename-self', 'Circular logic' );
+
+ $expected = [];
+
+ if ( $filter === null || $filter === '!local' ) {
+ $expected[] = [
+ 'prefix' => 'foreign',
+ 'url' => wfExpandUrl( '//foreign.example/wiki/$1', PROTO_CURRENT ),
+ 'protorel' => true,
+ ];
+ }
+ if ( $filter === null || $filter === 'local' ) {
+ $expected[] = [
+ 'prefix' => 'self',
+ 'local' => true,
+ 'trans' => true,
+ 'language' => 'Recursion',
+ 'localinterwiki' => true,
+ 'extralanglink' => true,
+ 'linktext' => 'Self!',
+ 'sitename' => 'Circular logic',
+ 'url' => "$wgServer$wgScriptPath/index.php?title=$1",
+ 'protorel' => false,
+ 'wikiid' => 'somedbname',
+ 'api' => "$wgServer$wgScriptPath/api.php",
+ ];
+ }
+
+ $data = $this->doQuery( 'interwikimap',
+ $filter === null ? [] : [ 'sifilteriw' => $filter ] );
+
+ $this->assertSame( $expected, $data );
+ }
+
+ public function interwikiMapProvider() {
+ return [ [ 'local' ], [ '!local' ], [ null ] ];
+ }
+
+ /**
+ * @dataProvider dbReplLagProvider
+ */
+ public function testDbReplLagInfo( $showHostnames, $includeAll ) {
+ if ( !$showHostnames && $includeAll ) {
+ $this->setExpectedApiException( 'apierror-siteinfo-includealldenied' );
+ }
+
+ $mockLB = $this->getMockBuilder( LoadBalancer::class )
+ ->disableOriginalConstructor()
+ ->setMethods( [ 'getMaxLag', 'getLagTimes', 'getServerName', '__destruct' ] )
+ ->getMock();
+ $mockLB->method( 'getMaxLag' )->willReturn( [ null, 7, 1 ] );
+ $mockLB->method( 'getLagTimes' )->willReturn( [ 5, 7 ] );
+ $mockLB->method( 'getServerName' )->will( $this->returnValueMap( [
+ [ 0, 'apple' ], [ 1, 'carrot' ]
+ ] ) );
+ $this->setService( 'DBLoadBalancer', $mockLB );
+
+ $this->setMwGlobals( 'wgShowHostnames', $showHostnames );
+
+ $expected = [];
+ if ( $includeAll ) {
+ $expected[] = [ 'host' => $showHostnames ? 'apple' : '', 'lag' => 5 ];
+ }
+ $expected[] = [ 'host' => $showHostnames ? 'carrot' : '', 'lag' => 7 ];
+
+ $data = $this->doQuery( 'dbrepllag', $includeAll ? [ 'sishowalldb' => '' ] : [] );
+
+ $this->assertSame( $expected, $data );
+ }
+
+ public function dbReplLagProvider() {
+ return [
+ 'no hostnames, no showalldb' => [ false, false ],
+ 'no hostnames, showalldb' => [ false, true ],
+ 'hostnames, no showalldb' => [ true, false ],
+ 'hostnames, showalldb' => [ true, true ]
+ ];
+ }
+
+ public function testStatistics() {
+ $this->setTemporaryHook( 'APIQuerySiteInfoStatisticsInfo',
+ function ( &$data ) {
+ $data['addedstats'] = 42;
+ }
+ );
+
+ $expected = [
+ 'pages' => intval( SiteStats::pages() ),
+ 'articles' => intval( SiteStats::articles() ),
+ 'edits' => intval( SiteStats::edits() ),
+ 'images' => intval( SiteStats::images() ),
+ 'users' => intval( SiteStats::users() ),
+ 'activeusers' => intval( SiteStats::activeUsers() ),
+ 'admins' => intval( SiteStats::numberingroup( 'sysop' ) ),
+ 'jobs' => intval( SiteStats::jobs() ),
+ 'addedstats' => 42,
+ ];
+
+ $this->assertSame( $expected, $this->doQuery( 'statistics' ) );
+ }
+
+ /**
+ * @dataProvider groupsProvider
+ */
+ public function testUserGroups( $numInGroup ) {
+ global $wgGroupPermissions, $wgAutopromote;
+
+ $this->setGroupPermissions( 'viscount', 'perambulate', 'yes' );
+ $this->setGroupPermissions( 'viscount', 'legislate', '0' );
+ $this->setMwGlobals( [
+ 'wgAddGroups' => [ 'viscount' => true, 'bot' => [] ],
+ 'wgRemoveGroups' => [ 'viscount' => [ 'sysop' ], 'bot' => [ '*', 'earl' ] ],
+ 'wgGroupsAddToSelf' => [ 'bot' => [ 'bureaucrat', 'sysop' ] ],
+ 'wgGroupsRemoveFromSelf' => [ 'bot' => [ 'bot' ] ],
+ ] );
+
+ $data = $this->doQuery( 'usergroups', $numInGroup ? [ 'sinumberingroup' => '' ] : [] );
+
+ $names = array_map(
+ function ( $val ) {
+ return $val['name'];
+ },
+ $data
+ );
+
+ $this->assertSame( array_keys( $wgGroupPermissions ), $names );
+
+ foreach ( $data as $val ) {
+ if ( !$numInGroup ) {
+ $expectedSize = null;
+ } elseif ( $val['name'] === 'user' ) {
+ $expectedSize = SiteStats::users();
+ } elseif ( $val['name'] === '*' || isset( $wgAutopromote[$val['name']] ) ) {
+ $expectedSize = null;
+ } else {
+ $expectedSize = SiteStats::numberingroup( $val['name'] );
+ }
+
+ if ( $expectedSize === null ) {
+ $this->assertArrayNotHasKey( 'number', $val );
+ } else {
+ $this->assertSame( $expectedSize, $val['number'] );
+ }
+
+ if ( $val['name'] === 'viscount' ) {
+ $viscountFound = true;
+ $this->assertSame( [ 'perambulate' ], $val['rights'] );
+ $this->assertSame( User::getAllGroups(), $val['add'] );
+ } elseif ( $val['name'] === 'bot' ) {
+ $this->assertArrayNotHasKey( 'add', $val );
+ $this->assertArrayNotHasKey( 'remove', $val );
+ $this->assertSame( [ 'bureaucrat', 'sysop' ], $val['add-self'] );
+ $this->assertSame( [ 'bot' ], $val['remove-self'] );
+ }
+ }
+ }
+
+ public function testFileExtensions() {
+ global $wgFileExtensions;
+
+ $this->stashMwGlobals( 'wgFileExtensions' );
+ // Add duplicate
+ $wgFileExtensions[] = 'png';
+
+ $expected = array_map(
+ function ( $val ) {
+ return [ 'ext' => $val ];
+ },
+ array_unique( $wgFileExtensions )
+ );
+
+ $this->assertSame( $expected, $this->doQuery( 'fileextensions' ) );
+ }
+
+ public function groupsProvider() {
+ return [
+ 'numingroup' => [ true ],
+ 'nonumingroup' => [ false ],
+ ];
+ }
+
+ public function testInstalledLibraries() {
+ // @todo Test no installed.json? Moving installed.json to a different name temporarily
+ // seems a bit scary, but I don't see any other way to do it.
+ //
+ // @todo Install extensions/skins somehow so that we can test they're filtered out
+ global $IP;
+
+ $path = "$IP/vendor/composer/installed.json";
+ if ( !file_exists( $path ) ) {
+ $this->markTestSkipped( 'No installed libraries' );
+ }
+
+ $expected = ( new ComposerInstalled( $path ) )->getInstalledDependencies();
+
+ $expected = array_filter( $expected,
+ function ( $info ) {
+ return strpos( $info['type'], 'mediawiki-' ) !== 0;
+ }
+ );
+
+ $expected = array_map(
+ function ( $name, $info ) {
+ return [ 'name' => $name, 'version' => $info['version'] ];
+ },
+ array_keys( $expected ),
+ array_values( $expected )
+ );
+
+ $this->assertSame( $expected, $this->doQuery( 'libraries' ) );
+ }
+
+ public function testExtensions() {
+ $tmpdir = $this->getNewTempDirectory();
+ touch( "$tmpdir/ErsatzExtension.php" );
+ touch( "$tmpdir/LICENSE" );
+ touch( "$tmpdir/AUTHORS.txt" );
+
+ $val = [
+ 'path' => "$tmpdir/ErsatzExtension.php",
+ 'name' => 'Ersatz Extension',
+ 'namemsg' => 'ersatz-extension-name',
+ 'author' => 'John Smith',
+ 'version' => '0.0.2',
+ 'url' => 'https://www.example.com/software/ersatz-extension',
+ 'description' => 'An extension that is not what it seems.',
+ 'descriptionmsg' => 'ersatz-extension-desc',
+ 'license-name' => 'PD',
+ ];
+
+ $this->setMwGlobals( 'wgExtensionCredits', [ 'api' => [
+ $val,
+ [
+ 'author' => [ 'John Smith', 'John Smith Jr.', '...' ],
+ 'descriptionmsg' => [ 'another-extension-desc', 'param' ] ],
+ ] ] );
+
+ $data = $this->doQuery( 'extensions' );
+
+ $this->assertCount( 2, $data );
+
+ $this->assertSame( 'api', $data[0]['type'] );
+
+ $sharedKeys = [ 'name', 'namemsg', 'description', 'descriptionmsg', 'author', 'url',
+ 'version', 'license-name' ];
+ foreach ( $sharedKeys as $key ) {
+ $this->assertSame( $val[$key], $data[0][$key] );
+ }
+
+ // @todo Test git info
+
+ $this->assertSame(
+ Title::newFromText( 'Special:Version/License/Ersatz Extension' )->getLinkURL(),
+ $data[0]['license']
+ );
+
+ $this->assertSame(
+ Title::newFromText( 'Special:Version/Credits/Ersatz Extension' )->getLinkURL(),
+ $data[0]['credits']
+ );
+
+ $this->assertSame( 'another-extension-desc', $data[1]['descriptionmsg'] );
+ $this->assertSame( [ 'param' ], $data[1]['descriptionmsgparams'] );
+ $this->assertSame( 'John Smith, John Smith Jr., ...', $data[1]['author'] );
+ }
+
+ /**
+ * @dataProvider rightsInfoProvider
+ */
+ public function testRightsInfo( $page, $url, $text, $expectedUrl, $expectedText ) {
+ $this->setMwGlobals( [
+ 'wgRightsPage' => $page,
+ 'wgRightsUrl' => $url,
+ 'wgRightsText' => $text,
+ ] );
+
+ $this->assertSame(
+ [ 'url' => $expectedUrl, 'text' => $expectedText ],
+ $this->doQuery( 'rightsinfo' )
+ );
+ }
+
+ public function rightsInfoProvider() {
+ $textUrl = wfExpandUrl( Title::newFromText( 'License' ), PROTO_CURRENT );
+ $url = 'http://license.example/';
+
+ return [
+ 'No rights info' => [ null, null, null, '', '' ],
+ 'Only page' => [ 'License', null, null, $textUrl, 'License' ],
+ 'Only URL' => [ null, $url, null, $url, '' ],
+ 'Only text' => [ null, null, '!!!', '', '!!!' ],
+ // URL is ignored if page is specified
+ 'Page and URL' => [ 'License', $url, null, $textUrl, 'License' ],
+ 'URL and text' => [ null, $url, '!!!', $url, '!!!' ],
+ 'Page and text' => [ 'License', null, '!!!', $textUrl, '!!!' ],
+ 'Page and URL and text' => [ 'License', $url, '!!!', $textUrl, '!!!' ],
+ 'Pagename "0"' => [ '0', null, null,
+ wfExpandUrl( Title::newFromText( '0' ), PROTO_CURRENT ), '0' ],
+ 'URL "0"' => [ null, '0', null, '0', '' ],
+ 'Text "0"' => [ null, null, '0', '', '0' ],
+ ];
+ }
+
+ public function testRestrictions() {
+ global $wgRestrictionTypes, $wgRestrictionLevels, $wgCascadingRestrictionLevels,
+ $wgSemiprotectedRestrictionLevels;
+
+ $this->assertSame( [
+ 'types' => $wgRestrictionTypes,
+ 'levels' => $wgRestrictionLevels,
+ 'cascadinglevels' => $wgCascadingRestrictionLevels,
+ 'semiprotectedlevels' => $wgSemiprotectedRestrictionLevels,
+ ], $this->doQuery( 'restrictions' ) );
+ }
+
+ /**
+ * @dataProvider languagesProvider
+ */
+ public function testLanguages( $langCode ) {
+ $expected = Language::fetchLanguageNames( (string)$langCode );
+
+ $expected = array_map(
+ function ( $code, $name ) {
+ return [
+ 'code' => $code,
+ 'name' => $name
+ ];
+ },
+ array_keys( $expected ),
+ array_values( $expected )
+ );
+
+ $data = $this->doQuery( 'languages',
+ $langCode !== null ? [ 'siinlanguagecode' => $langCode ] : [] );
+
+ $this->assertSame( $expected, $data );
+ }
+
+ public function languagesProvider() {
+ return [ [ null ], [ 'fr' ] ];
+ }
+
+ public function testLanguageVariants() {
+ $expectedKeys = array_filter( LanguageConverter::$languagesWithVariants,
+ function ( $langCode ) {
+ return !Language::factory( $langCode )->getConverter() instanceof FakeConverter;
+ }
+ );
+ sort( $expectedKeys );
+
+ $this->assertSame( $expectedKeys, array_keys( $this->doQuery( 'languagevariants' ) ) );
+ }
+
+ public function testLanguageVariantsDisabled() {
+ $this->setMwGlobals( 'wgDisableLangConversion', true );
+
+ $this->assertSame( [], $this->doQuery( 'languagevariants' ) );
+ }
+
+ /**
+ * @todo Test a skin with a description that's known to be different in a different language.
+ * Vector will do, but it's not installed by default.
+ *
+ * @todo Test that an invalid language code doesn't actually try reading any messages
+ *
+ * @dataProvider skinsProvider
+ */
+ public function testSkins( $code ) {
+ $data = $this->doQuery( 'skins', $code !== null ? [ 'siinlanguagecode' => $code ] : [] );
+
+ $expectedAllowed = Skin::getAllowedSkins();
+ $expectedDefault = Skin::normalizeKey( 'default' );
+
+ $i = 0;
+ foreach ( Skin::getSkinNames() as $name => $displayName ) {
+ $this->assertSame( $name, $data[$i]['code'] );
+
+ $msg = wfMessage( "skinname-$name" );
+ if ( $code && Language::isValidCode( $code ) ) {
+ $msg->inLanguage( $code );
+ } else {
+ $msg->inContentLanguage();
+ }
+ if ( $msg->exists() ) {
+ $displayName = $msg->text();
+ }
+ $this->assertSame( $displayName, $data[$i]['name'] );
+
+ if ( !isset( $expectedAllowed[$name] ) ) {
+ $this->assertTrue( $data[$i]['unusable'], "$name must be unusable" );
+ }
+ if ( $name === $expectedDefault ) {
+ $this->assertTrue( $data[$i]['default'], "$expectedDefault must be default" );
+ }
+ $i++;
+ }
+ }
+
+ public function skinsProvider() {
+ return [
+ 'No language specified' => [ null ],
+ 'Czech' => [ 'cs' ],
+ 'Invalid language' => [ '/invalid/' ],
+ ];
+ }
+
+ public function testExtensionTags() {
+ global $wgParser;
+
+ $wgParser->firstCallInit();
+ $expected = array_map(
+ function ( $tag ) {
+ return "<$tag>";
+ },
+ $wgParser->getTags()
+ );
+
+ $this->assertSame( $expected, $this->doQuery( 'extensiontags' ) );
+ }
+
+ public function testFunctionHooks() {
+ global $wgParser;
+
+ $wgParser->firstCallInit();
+ $this->assertSame( $wgParser->getFunctionHooks(), $this->doQuery( 'functionhooks' ) );
+ }
+
+ public function testVariables() {
+ $this->assertSame( MagicWord::getVariableIDs(), $this->doQuery( 'variables' ) );
+ }
+
+ public function testProtocols() {
+ global $wgUrlProtocols;
+
+ $this->assertSame( $wgUrlProtocols, $this->doQuery( 'protocols' ) );
+ }
+
+ public function testDefaultOptions() {
+ $this->assertSame( User::getDefaultOptions(), $this->doQuery( 'defaultoptions' ) );
+ }
+
+ public function testUploadDialog() {
+ global $wgUploadDialog;
+
+ $this->assertSame( $wgUploadDialog, $this->doQuery( 'uploaddialog' ) );
+ }
+
+ public function testGetHooks() {
+ global $wgHooks;
+
+ // Make sure there's something to report on
+ $this->setTemporaryHook( 'somehook',
+ function () {
+ return;
+ }
+ );
+
+ $expectedNames = $wgHooks;
+ ksort( $expectedNames );
+
+ $actualNames = array_map(
+ function ( $val ) {
+ return $val['name'];
+ },
+ $this->doQuery( 'showhooks' )
+ );
+
+ $this->assertSame( array_keys( $expectedNames ), $actualNames );
+ }
+
+ public function testContinuation() {
+ // We make lots and lots of URL protocols that are each 100 bytes
+ global $wgAPIMaxResultSize, $wgUrlProtocols;
+
+ $this->setMwGlobals( 'wgUrlProtocols', [] );
+
+ // Just under the limit
+ $chunks = $wgAPIMaxResultSize / 100 - 1;
+
+ for ( $i = 0; $i < $chunks; $i++ ) {
+ $wgUrlProtocols[] = substr( str_repeat( "$i ", 50 ), 0, 100 );
+ }
+
+ $res = $this->doApiRequest( [
+ 'action' => 'query',
+ 'meta' => 'siteinfo',
+ 'siprop' => 'protocols|languages',
+ ] );
+
+ $this->assertSame(
+ wfMessage( 'apiwarn-truncatedresult', Message::numParam( $wgAPIMaxResultSize ) )
+ ->text(),
+ $res[0]['warnings']['result']['warnings']
+ );
+
+ $this->assertSame( $wgUrlProtocols, $res[0]['query']['protocols'] );
+ $this->assertArrayNotHasKey( 'languages', $res[0] );
+ $this->assertTrue( $res[0]['batchcomplete'], 'batchcomplete should be true' );
+ $this->assertSame( [ 'siprop' => 'languages', 'continue' => '-||' ], $res[0]['continue'] );
+ }
+}
'ApiTestCase::setUp can be slow, tests must be "medium" or "large"'
);
}
+
+ /**
+ * Expect an ApiUsageException to be thrown with the given parameters, which are the same as
+ * ApiUsageException::newWithMessage()'s parameters. This allows checking for an exception
+ * whose text is given by a message key instead of text, so as not to hard-code the message's
+ * text into test code.
+ */
+ protected function setExpectedApiException(
+ $msg, $code = null, array $data = null, $httpCode = 0
+ ) {
+ $expected = ApiUsageException::newWithMessage( null, $msg, $code, $data, $httpCode );
+ $this->setExpectedException( ApiUsageException::class, $expected->getMessage() );
+ }
}
return new \Message( $key, $params, \Language::factory( 'en' ) );
}
+ /**
+ * Test two AuthenticationResponses for equality. We don't want to use regular assertEquals
+ * because that recursively compares members, which leads to false negatives if e.g. Language
+ * caches are reset.
+ *
+ * @param AuthenticationResponse $response1
+ * @param AuthenticationResponse $response2
+ * @param string $msg
+ * @return bool
+ */
+ private function assertResponseEquals(
+ AuthenticationResponse $expected, AuthenticationResponse $actual, $msg = ''
+ ) {
+ foreach ( ( new \ReflectionClass( $expected ) )->getProperties() as $prop ) {
+ $name = $prop->getName();
+ $usedMsg = ltrim( "$msg ($name)" );
+ if ( $name === 'message' && $expected->message ) {
+ $this->assertSame( $expected->message->serialize(), $actual->message->serialize(),
+ $usedMsg );
+ } else {
+ $this->assertEquals( $expected->$name, $actual->$name, $usedMsg );
+ }
+ }
+ }
+
/**
* Initialize the AuthManagerConfig variable in $this->config
*
$this->assertSame( 'http://localhost/', $req->returnToUrl );
$ret->message = $this->message( $ret->message );
- $this->assertEquals( $response, $ret, "Response $i, response" );
+ $this->assertResponseEquals( $response, $ret, "Response $i, response" );
if ( $success ) {
$this->assertSame( $id, $session->getUser()->getId(),
"Response $i, authn" );
"Response $i, login marker" );
}
$ret->message = $this->message( $ret->message );
- $this->assertEquals( $response, $ret, "Response $i, response" );
+ $this->assertResponseEquals( $response, $ret, "Response $i, response" );
if ( $success || $response->status === AuthenticationResponse::FAIL ) {
$this->assertNull(
$this->request->getSession()->getSecret( 'AuthManager::accountCreationState' ),
$this->assertSame( 'http://localhost/', $req->returnToUrl );
$ret->message = $this->message( $ret->message );
- $this->assertEquals( $response, $ret, "Response $i, response" );
+ $this->assertResponseEquals( $response, $ret, "Response $i, response" );
if ( $response->status === AuthenticationResponse::PASS ||
$response->status === AuthenticationResponse::FAIL
) {
$user = self::getMutableTestUser()->getUser();
$dbw = wfGetDB( DB_MASTER );
-
- $passwordFactory = new \PasswordFactory();
- $passwordFactory->init( \RequestContext::getMain()->getConfig() );
+ $config = MediaWikiServices::getInstance()->getMainConfig();
// A is unsalted MD5 (thus fast) ... we don't care about security here, this is test only
- $passwordFactory->setDefaultType( 'A' );
+ $passwordFactory = new \PasswordFactory( $config->get( 'PasswordConfig' ), 'A' );
+
$pwhash = $passwordFactory->newFromPlaintext( 'password' )->toString();
$provider = $this->getProvider();
class ContentHandlerTest extends MediaWikiTestCase {
protected function setUp() {
- global $wgContLang;
parent::setUp();
$this->setMwGlobals( [
],
] );
- // Reset namespace cache
- MWNamespace::clearCaches();
- $wgContLang->resetNamespaces();
- // And LinkCache
+ // Reset LinkCache
MediaWikiServices::getInstance()->resetServiceForTesting( 'LinkCache' );
}
protected function tearDown() {
- global $wgContLang;
-
- // Reset namespace cache
- MWNamespace::clearCaches();
- $wgContLang->resetNamespaces();
- // And LinkCache
+ // Reset LinkCache
MediaWikiServices::getInstance()->resetServiceForTesting( 'LinkCache' );
parent::tearDown();
$file = $this->dataFile( $name, $type );
$thumb = $file->transform( $params, File::RENDER_NOW | File::RENDER_FORCE );
+ if ( $thumb->isError() ) {
+ /** @var MediaTransformError $thumb */
+ $this->fail( $thumb->toText() );
+ }
+
$this->assertEquals(
$out[0],
$thumb->getWidth(),
* @covers PasswordFactory
*/
class PasswordFactoryTest extends MediaWikiTestCase {
+ public function testConstruct() {
+ $pf = new PasswordFactory();
+ $this->assertEquals( [ '' ], array_keys( $pf->getTypes() ) );
+ $this->assertEquals( '', $pf->getDefaultType() );
+
+ $pf = new PasswordFactory( [
+ 'foo' => [ 'class' => 'FooPassword' ],
+ 'bar' => [ 'class' => 'BarPassword', 'baz' => 'boom' ],
+ ], 'foo' );
+ $this->assertEquals( [ '', 'foo', 'bar' ], array_keys( $pf->getTypes() ) );
+ $this->assertArraySubset( [ 'class' => 'BarPassword', 'baz' => 'boom' ], $pf->getTypes()['bar'] );
+ $this->assertEquals( 'foo', $pf->getDefaultType() );
+ }
+
public function testRegister() {
$pf = new PasswordFactory;
$pf->register( 'foo', [ 'class' => InvalidPassword::class ] );
namespace MediaWiki\Session;
+use MediaWiki\MediaWikiServices;
use Psr\Log\LogLevel;
use MediaWikiTestCase;
use Wikimedia\TestingAccessWrapper;
}
public function addDBDataOnce() {
- $passwordFactory = new \PasswordFactory();
- $passwordFactory->init( \RequestContext::getMain()->getConfig() );
+ $passwordFactory = MediaWikiServices::getInstance()->getPasswordFactory();
$passwordHash = $passwordFactory->newFromPlaintext( 'foobaz' );
$sysop = static::getTestSysop()->getUser();
<?php
+use MediaWiki\MediaWikiServices;
use MediaWiki\Session\SessionManager;
use Wikimedia\ScopedCallback;
use Wikimedia\TestingAccessWrapper;
}
public function addDBData() {
- $passwordFactory = new \PasswordFactory();
- $passwordFactory->init( \RequestContext::getMain()->getConfig() );
+ $passwordFactory = MediaWikiServices::getInstance()->getPasswordFactory();
$passwordHash = $passwordFactory->newFromPlaintext( 'foobaz' );
$dbw = wfGetDB( DB_MASTER );
* @param string|null $password
*/
public function testSave( $password ) {
- $passwordFactory = new \PasswordFactory();
- $passwordFactory->init( \RequestContext::getMain()->getConfig() );
+ $passwordFactory = MediaWikiServices::getInstance()->getPasswordFactory();
$bp = BotPassword::newUnsaved( [
'centralId' => 42,
<?php
+use Wikimedia\TestingAccessWrapper;
+
class LanguageTest extends LanguageClassesTestCase {
/**
* @covers Language::convertDoubleWidth
$this->assertEquals( "a{$c}b{$c}c{$and}{$s}d", $lang->listToText( [ 'a', 'b', 'c', 'd' ] ) );
}
+ /**
+ * @covers Language::clearCaches
+ */
+ public function testClearCaches() {
+ $languageClass = TestingAccessWrapper::newFromClass( Language::class );
+
+ // Populate $dataCache
+ Language::getLocalisationCache()->getItem( 'zh', 'mainpage' );
+ $oldCacheObj = Language::$dataCache;
+ $this->assertNotCount( 0,
+ TestingAccessWrapper::newFromObject( Language::$dataCache )->loadedItems );
+
+ // Populate $mLangObjCache
+ $lang = Language::factory( 'en' );
+ $this->assertNotCount( 0, Language::$mLangObjCache );
+
+ // Populate $fallbackLanguageCache
+ Language::getFallbacksIncludingSiteLanguage( 'en' );
+ $this->assertNotCount( 0, $languageClass->fallbackLanguageCache );
+
+ // Populate $grammarTransformations
+ $lang->getGrammarTransformations();
+ $this->assertNotNull( $languageClass->grammarTransformations );
+
+ // Populate $languageNameCache
+ Language::fetchLanguageNames();
+ $this->assertNotNull( $languageClass->languageNameCache );
+
+ Language::clearCaches();
+
+ $this->assertNotSame( $oldCacheObj, Language::$dataCache );
+ $this->assertCount( 0,
+ TestingAccessWrapper::newFromObject( Language::$dataCache )->loadedItems );
+ $this->assertCount( 0, Language::$mLangObjCache );
+ $this->assertCount( 0, $languageClass->fallbackLanguageCache );
+ $this->assertNull( $languageClass->grammarTransformations );
+ $this->assertNull( $languageClass->languageNameCache );
+ }
+
/**
* @dataProvider provideIsSupportedLanguage
* @covers Language::isSupportedLanguage