-Change notes from older releases. For current info see RELEASE-NOTES-1.33.
+Change notes from older releases. For current info see RELEASE-NOTES-1.34.
= MediaWiki 1.32 =
THIS IS NOT A RELEASE YET
-MediaWiki 1.33 is an alpha-quality branch and is not recommended for use in
-production.
+MediaWiki 1.33 is a pre-release testing branch, and is not recommended for use
+in production.
== Upgrading notes for 1.33 ==
1.33 has several database changes since 1.32, and will not work without schema
For classes that inherit from MediaWikiTestCase and used setMwGlobals() to
modify a variable that affects namespaces, caches will automatically be
reset and any calls to MWNamespace::clearCaches() can be removed entirely.
+* ReadOnlyMode::clearCache() and ConfiguredReadOnlyMode::clearCache() have been
+ removed. Use MediaWikiTestCase::overrideMwServices() instead.
=== Deprecations in 1.33 ===
* The configuration option $wgUseESI has been deprecated, and is expected
--- /dev/null
+= MediaWiki 1.34 =
+
+== MediaWiki 1.34.0-PRERELEASE ==
+
+THIS IS NOT A RELEASE YET
+
+MediaWiki 1.34 is an alpha-quality development branch, and is not recommended
+for use in production.
+
+== Upgrading notes for 1.34 ==
+1.34 has several database changes since 1.33, and will not work without schema
+updates. Note that due to changes to some very large tables like the revision
+table, the schema update may take quite long (minutes on a medium sized site,
+many hours on a large site).
+
+Don't forget to always back up your database before upgrading!
+
+See the file UPGRADE for more detailed upgrade instructions, including
+important information when upgrading from versions prior to 1.11.
+
+Some specific notes for MediaWiki 1.34 upgrades are below:
+
+* …
+
+For notes on 1.33.x and older releases, see HISTORY.
+
+=== Configuration changes for system administrators in 1.34 ===
+==== New configuration ====
+* …
+
+==== Changed configuration ====
+* …
+
+==== Removed configuration ====
+* …
+
+=== New user-facing features in 1.34 ===
+* …
+
+=== New developer features in 1.34 ===
+* …
+
+=== External library changes in 1.34 ===
+==== New external libraries ====
+* …
+
+==== Changed external libraries ====
+* …
+
+==== Removed external libraries ====
+* …
+
+=== Bug fixes in 1.34 ===
+* …
+
+=== Action API changes in 1.34 ===
+* …
+
+=== Action API internal changes in 1.34 ===
+* …
+
+=== Languages updated in 1.34 ===
+MediaWiki supports over 350 languages. Many localisations are updated regularly.
+Below only new and removed languages are listed, as well as changes to languages
+because of Phabricator reports.
+
+* …
+
+=== Breaking changes in 1.34 ===
+* …
+
+=== Deprecations in 1.34 ===
+* …
+
+=== Other changes in 1.34 ===
+* …
+
+== Compatibility ==
+MediaWiki 1.34 requires PHP 7.0.13 or later. Although HHVM 3.18.5 or later is
+supported, it is generally advised to use PHP 7.0.13 or later for long term
+support.
+
+MySQL/MariaDB is the recommended DBMS. PostgreSQL or SQLite can also be used,
+but support for them is somewhat less mature. There is experimental support for
+Oracle and Microsoft SQL Server.
+
+The supported versions are:
+
+* MySQL 5.5.8 or later
+* PostgreSQL 9.2 or later
+* SQLite 3.8.0 or later
+* Oracle 9.0.1 or later
+* Microsoft SQL Server 2005 (9.00.1399)
+
+== Online documentation ==
+Documentation for both end-users and site administrators is available on
+MediaWiki.org, and is covered under the GNU Free Documentation License (except
+for pages that explicitly state that their contents are in the public domain):
+
+ https://www.mediawiki.org/wiki/Special:MyLanguage/Documentation
+
+== Mailing list ==
+A mailing list is available for MediaWiki user support and discussion:
+
+ https://lists.wikimedia.org/mailman/listinfo/mediawiki-l
+
+A low-traffic announcements-only list is also available:
+
+ https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce
+
+It's highly recommended that you sign up for one of these lists if you're
+going to run a public MediaWiki, so you can be notified of security fixes.
+
+== IRC help ==
+There's usually someone online in #mediawiki on irc.freenode.net.
'DatabaseUpdater' => __DIR__ . '/includes/installer/DatabaseUpdater.php',
'DateFormats' => __DIR__ . '/maintenance/language/date-formats.php',
'DateFormatter' => __DIR__ . '/includes/parser/DateFormatter.php',
+ 'DateFormatterFactory' => __DIR__ . '/includes/parser/DateFormatterFactory.php',
'DeadendPagesPage' => __DIR__ . '/includes/specials/SpecialDeadendpages.php',
'DeduplicateArchiveRevId' => __DIR__ . '/maintenance/deduplicateArchiveRevId.php',
'DeferrableCallback' => __DIR__ . '/includes/deferred/DeferrableCallback.php',
'GenerateNormalizerDataMl' => __DIR__ . '/maintenance/language/generateNormalizerDataMl.php',
'GenerateSitemap' => __DIR__ . '/maintenance/generateSitemap.php',
'GenericArrayObject' => __DIR__ . '/includes/libs/GenericArrayObject.php',
+ 'GenericParameterJob' => __DIR__ . '/includes/jobqueue/GenericParameterJob.php',
'GetConfiguration' => __DIR__ . '/maintenance/getConfiguration.php',
'GetLagTimes' => __DIR__ . '/maintenance/getLagTimes.php',
'GetReplicaServer' => __DIR__ . '/maintenance/getReplicaServer.php',
'RowUpdateGenerator' => __DIR__ . '/includes/utils/RowUpdateGenerator.php',
'RunBatchedQuery' => __DIR__ . '/maintenance/runBatchedQuery.php',
'RunJobs' => __DIR__ . '/maintenance/runJobs.php',
+ 'RunnableJob' => __DIR__ . '/includes/jobqueue/RunnableJob.php',
'SVGMetadataExtractor' => __DIR__ . '/includes/media/SVGMetadataExtractor.php',
'SVGReader' => __DIR__ . '/includes/media/SVGMetadataExtractor.php',
'SamplingStatsdClient' => __DIR__ . '/includes/libs/stats/SamplingStatsdClient.php',
public function setReason( $msg ) {
$this->overrideReason = $msg;
}
-
- /**
- * Clear the cache of the read only file
- */
- public function clearCache() {
- $this->fileReason = null;
- }
}
* MediaWiki version number
* @since 1.2
*/
-$wgVersion = '1.33.0-alpha';
+$wgVersion = '1.34.0-alpha';
/**
* Name of the site. It must be changed in LocalSettings.php
use ConfigFactory;
use CryptHKDF;
use CryptRand;
+use DateFormatterFactory;
use EventRelayerGroup;
use GenderCache;
use GlobalVarConfig;
return $this->getService( 'CryptRand' );
}
+ /**
+ * @since 1.33
+ * @return DateFormatterFactory
+ */
+ public function getDateFormatterFactory() {
+ return $this->getService( 'DateFormatterFactory' );
+ }
+
/**
* @since 1.28
* @return LoadBalancer The main DB load balancer for the local wiki.
return $this->getService( 'MimeAnalyzer' );
}
- /**
- * @since 1.32
- * @return NameTableStoreFactory
- */
- public function getNameTableStoreFactory() {
- return $this->getService( 'NameTableStoreFactory' );
- }
-
/**
* @since 1.33
* @return NamespaceInfo
return $this->getService( 'NamespaceInfo' );
}
+ /**
+ * @since 1.32
+ * @return NameTableStoreFactory
+ */
+ public function getNameTableStoreFactory() {
+ return $this->getService( 'NameTableStoreFactory' );
+ }
+
/**
* @since 1.32
* @return OldRevisionImporter
*/
class PHPVersionCheck {
/* @var string The number of the MediaWiki version used. */
- var $mwVersion = '1.33';
+ var $mwVersion = '1.34';
/* @var array A mapping of PHP functions to PHP extensions. */
var $functionsExtensionsMapping = array(
public function setReason( $msg ) {
$this->configuredReadOnly->setReason( $msg );
}
-
- /**
- * Clear the cache of the read only file
- */
- public function clearCache() {
- $this->configuredReadOnly->clearCache();
- }
}
return new CryptRand();
},
+ 'DateFormatterFactory' => function () : DateFormatterFactory {
+ return new DateFormatterFactory;
+ },
+
'DBLoadBalancer' => function ( MediaWikiServices $services ) : Wikimedia\Rdbms\LoadBalancer {
// just return the default LB from the DBLoadBalancerFactory service
return $services->getDBLoadBalancerFactory()->getMainLB();
return new MimeAnalyzer( $params );
},
+ 'NamespaceInfo' => function ( MediaWikiServices $services ) : NamespaceInfo {
+ return new NamespaceInfo( $services->getMainConfig() );
+ },
+
'NameTableStoreFactory' => function ( MediaWikiServices $services ) : NameTableStoreFactory {
return new NameTableStoreFactory(
$services->getDBLoadBalancerFactory(),
);
},
- 'NamespaceInfo' => function ( MediaWikiServices $services ) : NamespaceInfo {
- return new NamespaceInfo( $services->getMainConfig() );
- },
-
'OldRevisionImporter' => function ( MediaWikiServices $services ) : OldRevisionImporter {
return new ImportableOldRevisionImporter(
true,
},
'SpecialPageFactory' => function ( MediaWikiServices $services ) : SpecialPageFactory {
+ $config = $services->getMainConfig();
+ $options = [];
+ foreach ( SpecialPageFactory::$constructorOptions as $key ) {
+ $options[$key] = $config->get( $key );
+ }
return new SpecialPageFactory(
- $services->getMainConfig(),
+ $options,
$services->getContentLanguage()
);
},
* @ingroup Database
*/
-use MediaWiki\MediaWikiServices;
+use Wikimedia\Timestamp\ConvertibleTimestamp;
use Wikimedia\Rdbms\Database;
use Wikimedia\Rdbms\DatabaseDomain;
use Wikimedia\Rdbms\Blob;
function __construct( array $p ) {
$p['tablePrefix'] = strtoupper( $p['tablePrefix'] );
parent::__construct( $p );
+
+ // @TODO: dependency inject
Hooks::run( 'DatabaseOraclePostInit', [ $this ] );
}
}
protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix ) {
- global $wgDBOracleDRCP;
-
if ( !function_exists( 'oci_connect' ) ) {
throw new DBConnectionError(
$this,
return null;
}
- if ( $wgDBOracleDRCP ) {
- $this->setFlag( DBO_PERSISTENT );
- }
-
$session_mode = ( $this->flags & DBO_SYSDBA ) ? OCI_SYSDBA : OCI_DEFAULT;
Wikimedia\suppressWarnings();
*/
protected function doQuery( $sql ) {
wfDebug( "SQL: [$sql]\n" );
- if ( !StringUtils::isUtf8( $sql ) ) {
- throw new InvalidArgumentException( "SQL encoding is invalid\n$sql" );
+ if ( !mb_check_encoding( (string)$sql, 'UTF-8' ) ) {
+ throw new DBUnexpectedError( $this, "SQL encoding is invalid\n$sql" );
}
// handle some oracle specifics
}
if ( $val === null ) {
- if ( $col_info != false && $col_info->isNullable() == 0 && $col_info->defaultValue() != null ) {
+ if (
+ $col_info != false &&
+ $col_info->isNullable() == 0 &&
+ $col_info->defaultValue() != null
+ ) {
$bind .= 'DEFAULT';
} else {
$bind .= 'NULL';
}
// backward compatibility
- if ( preg_match( '/^timestamp.*/i', $col_type ) == 1 && strtolower( $val ) == 'infinity' ) {
+ if (
+ preg_match( '/^timestamp.*/i', $col_type ) == 1 &&
+ strtolower( $val ) == 'infinity'
+ ) {
$val = $this->getInfinity();
}
- $val = MediaWikiServices::getInstance()->getContentLanguage()->
- checkTitleEncoding( $val );
+ $val = $this->getVerifiedUTF8( $val );
if ( oci_bind_by_name( $stmt, ":$col", $val, -1, SQLT_CHR ) === false ) {
$e = oci_error( $stmt );
$this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ );
$lob[$col] = oci_new_descriptor( $this->conn, OCI_D_LOB );
if ( $lob[$col] === false ) {
$e = oci_error( $stmt );
- throw new DBUnexpectedError( $this, "Cannot create LOB descriptor: " . $e['message'] );
+ throw new DBUnexpectedError(
+ $this,
+ "Cannot create LOB descriptor: " . $e['message']
+ );
}
if ( is_object( $val ) ) {
if ( $sequenceData !== false &&
!isset( $varMap[$sequenceData['column']] )
) {
- $varMap[$sequenceData['column']] = 'GET_SEQUENCE_VALUE(\'' . $sequenceData['sequence'] . '\')';
+ $varMap[$sequenceData['column']] =
+ 'GET_SEQUENCE_VALUE(\'' . $sequenceData['sequence'] . '\')';
}
// count-alias subselect fields to avoid abigious definition errors
$selectJoinConds
);
- $sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ') ' . $selectSql;
+ $sql = "INSERT INTO $destTable (" .
+ implode( ',', array_keys( $varMap ) ) . ') ' . $selectSql;
if ( in_array( 'IGNORE', $insertOptions ) ) {
$this->ignoreDupValOnIndex = true;
return $this->doQuery( "DROP TABLE $tableName CASCADE CONSTRAINTS PURGE" );
}
- function timestamp( $ts = 0 ) {
- return wfTimestamp( TS_ORACLE, $ts );
+ public function timestamp( $ts = 0 ) {
+ $t = new ConvertibleTimestamp( $ts );
+ // Let errors bubble up to avoid putting garbage in the DB
+ return $t->getTimestamp( TS_ORACLE );
}
/**
*/
function fieldInfo( $table, $field ) {
if ( is_array( $table ) ) {
- throw new DBUnexpectedError( $this, 'DatabaseOracle::fieldInfo called with table array!' );
+ throw new DBUnexpectedError(
+ $this,
+ 'DatabaseOracle::fieldInfo called with table array!'
+ );
}
return $this->fieldInfoMulti( $table, $field );
}
function addQuotes( $s ) {
- $contLang = MediaWikiServices::getInstance()->getContentLanguage();
- if ( isset( $contLang->mLoaded ) && $contLang->mLoaded ) {
- $s = $contLang->checkTitleEncoding( $s );
- }
-
- return "'" . $this->strencode( $s ) . "'";
+ return "'" . $this->strencode( $this->getVerifiedUTF8( $s ) ) . "'";
}
public function addIdentifierQuotes( $s ) {
$col_type = $col_info != false ? $col_info->type() : 'CONSTANT';
if ( $col_type == 'CLOB' ) {
$col = 'TO_CHAR(' . $col . ')';
- $val =
- MediaWikiServices::getInstance()->getContentLanguage()->checkTitleEncoding( $val );
+ $val = $this->getVerifiedUTF8( $val );
} elseif ( $col_type == 'VARCHAR2' ) {
- $val =
- MediaWikiServices::getInstance()->getContentLanguage()->checkTitleEncoding( $val );
+ $val = $this->getVerifiedUTF8( $val );
}
}
$val = $val->getData();
}
- if ( preg_match( '/^timestamp.*/i', $col_type ) == 1 && strtolower( $val ) == 'infinity' ) {
+ if (
+ preg_match( '/^timestamp.*/i', $col_type ) == 1 &&
+ strtolower( $val ) == 'infinity'
+ ) {
$val = '31-12-2030 12:00:00.000000';
}
- $val = MediaWikiServices::getInstance()->getContentLanguage()->
- checkTitleEncoding( $val );
+ $val = $this->getVerifiedUTF8( $val );
if ( oci_bind_by_name( $stmt, ":$col", $val ) === false ) {
$e = oci_error( $stmt );
$this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ );
$lob[$col] = oci_new_descriptor( $this->conn, OCI_D_LOB );
if ( $lob[$col] === false ) {
$e = oci_error( $stmt );
- throw new DBUnexpectedError( $this, "Cannot create LOB descriptor: " . $e['message'] );
+ throw new DBUnexpectedError(
+ $this,
+ "Cannot create LOB descriptor: " . $e['message']
+ );
}
if ( is_object( $val ) ) {
public function getInfinity() {
return '31-12-2030 12:00:00.000000';
}
+
+ /**
+ * @param string $s
+ * @return string
+ */
+ private function getVerifiedUTF8( $s ) {
+ if ( mb_check_encoding( (string)$s, 'UTF-8' ) ) {
+ return $s; // valid
+ }
+
+ throw new DBUnexpectedError( $this, "Non BLOB/CLOB field must be UTF-8." );
+ }
}
) {
global $wgCommandLineMode;
- static $typesWithSchema = [ 'postgres', 'msssql' ];
+ $typesWithSchema = self::getDbTypesWithSchemas();
$lbConf += [
'localDomain' => new DatabaseDomain(
// for Database classes in the relevant Installer subclass.
// Such as MysqlInstaller::openConnection and PostgresInstaller::openConnectionWithParams.
if ( $lbConf['class'] === Wikimedia\Rdbms\LBFactorySimple::class ) {
- $httpMethod = $_SERVER['REQUEST_METHOD'] ?? null;
- // T93097: hint for how file-based databases (e.g. sqlite) should go about locking.
- // See https://www.sqlite.org/lang_transaction.html
- // See https://www.sqlite.org/lockingv3.html#shared_lock
- $isReadRequest = in_array( $httpMethod, [ 'GET', 'HEAD', 'OPTIONS', 'TRACE' ] );
-
if ( isset( $lbConf['servers'] ) ) {
- // Server array is already explicitly configured; leave alone
+ // Server array is already explicitly configured
} elseif ( is_array( $mainConfig->get( 'DBservers' ) ) ) {
$lbConf['servers'] = [];
foreach ( $mainConfig->get( 'DBservers' ) as $i => $server ) {
- if ( $server['type'] === 'sqlite' ) {
- $server += [
- 'dbDirectory' => $mainConfig->get( 'SQLiteDataDir' ),
- 'trxMode' => $isReadRequest ? 'DEFERRED' : 'IMMEDIATE'
- ];
- } elseif ( $server['type'] === 'postgres' ) {
- $server += [
- 'port' => $mainConfig->get( 'DBport' ),
- // Work around the reserved word usage in MediaWiki schema
- 'keywordTableMap' => [ 'user' => 'mwuser', 'text' => 'pagecontent' ]
- ];
- } elseif ( $server['type'] === 'mssql' ) {
- $server += [
- 'port' => $mainConfig->get( 'DBport' ),
- 'useWindowsAuth' => $mainConfig->get( 'DBWindowsAuthentication' )
- ];
- }
-
- if ( in_array( $server['type'], $typesWithSchema, true ) ) {
- $server += [ 'schema' => $mainConfig->get( 'DBmwschema' ) ];
- }
-
- $server += [
- 'tablePrefix' => $mainConfig->get( 'DBprefix' ),
- 'flags' => DBO_DEFAULT,
- 'sqlMode' => $mainConfig->get( 'SQLMode' ),
- ];
-
- $lbConf['servers'][$i] = $server;
+ $lbConf['servers'][$i] = self::initServerInfo( $server, $mainConfig );
}
} else {
- $flags = DBO_DEFAULT;
- $flags |= $mainConfig->get( 'DebugDumpSql' ) ? DBO_DEBUG : 0;
- $flags |= $mainConfig->get( 'DBssl' ) ? DBO_SSL : 0;
- $flags |= $mainConfig->get( 'DBcompress' ) ? DBO_COMPRESS : 0;
- $server = [
- 'host' => $mainConfig->get( 'DBserver' ),
- 'user' => $mainConfig->get( 'DBuser' ),
- 'password' => $mainConfig->get( 'DBpassword' ),
- 'dbname' => $mainConfig->get( 'DBname' ),
- 'tablePrefix' => $mainConfig->get( 'DBprefix' ),
- 'type' => $mainConfig->get( 'DBtype' ),
- 'load' => 1,
- 'flags' => $flags,
- 'sqlMode' => $mainConfig->get( 'SQLMode' ),
- 'trxMode' => $isReadRequest ? 'DEFERRED' : 'IMMEDIATE'
- ];
- if ( in_array( $server['type'], $typesWithSchema, true ) ) {
- $server += [ 'schema' => $mainConfig->get( 'DBmwschema' ) ];
- }
- if ( $server['type'] === 'sqlite' ) {
- $server[ 'dbDirectory'] = $mainConfig->get( 'SQLiteDataDir' );
- } elseif ( $server['type'] === 'postgres' ) {
- $server['port'] = $mainConfig->get( 'DBport' );
- // Work around the reserved word usage in MediaWiki schema
- $server['keywordTableMap'] = [ 'user' => 'mwuser', 'text' => 'pagecontent' ];
- } elseif ( $server['type'] === 'mssql' ) {
- $server['port'] = $mainConfig->get( 'DBport' );
- $server['useWindowsAuth'] = $mainConfig->get( 'DBWindowsAuthentication' );
- }
+ $server = self::initServerInfo(
+ [
+ 'host' => $mainConfig->get( 'DBserver' ),
+ 'user' => $mainConfig->get( 'DBuser' ),
+ 'password' => $mainConfig->get( 'DBpassword' ),
+ 'dbname' => $mainConfig->get( 'DBname' ),
+ 'type' => $mainConfig->get( 'DBtype' ),
+ 'load' => 1
+ ],
+ $mainConfig
+ );
+
+ $server['flags'] |= $mainConfig->get( 'DBssl' ) ? DBO_SSL : 0;
+ $server['flags'] |= $mainConfig->get( 'DBcompress' ) ? DBO_COMPRESS : 0;
+
$lbConf['servers'] = [ $server ];
}
if ( !isset( $lbConf['externalClusters'] ) ) {
}
$lbConf['serverTemplate']['sqlMode'] = $mainConfig->get( 'SQLMode' );
}
- $serversCheck = $lbConf['serverTemplate'] ?? [];
+ $serversCheck = [ $lbConf['serverTemplate'] ] ?? [];
}
- self::sanityCheckServerConfig( $serversCheck, $mainConfig );
- $lbConf = self::applyDefaultCaching( $lbConf, $srvCace, $mainStash, $wanCache );
+ self::assertValidServerConfigs( $serversCheck, $mainConfig );
+
+ $lbConf = self::injectObjectCaches( $lbConf, $srvCace, $mainStash, $wanCache );
return $lbConf;
}
+ /**
+ * @return array
+ */
+ private static function getDbTypesWithSchemas() {
+ return [ 'postgres', 'msssql' ];
+ }
+
+ /**
+ * @param array $server
+ * @param Config $mainConfig
+ * @return array
+ */
+ private static function initServerInfo( array $server, Config $mainConfig ) {
+ if ( $server['type'] === 'sqlite' ) {
+ $httpMethod = $_SERVER['REQUEST_METHOD'] ?? null;
+ // T93097: hint for how file-based databases (e.g. sqlite) should go about locking.
+ // See https://www.sqlite.org/lang_transaction.html
+ // See https://www.sqlite.org/lockingv3.html#shared_lock
+ $isHttpRead = in_array( $httpMethod, [ 'GET', 'HEAD', 'OPTIONS', 'TRACE' ] );
+ $server += [
+ 'dbDirectory' => $mainConfig->get( 'SQLiteDataDir' ),
+ 'trxMode' => $isHttpRead ? 'DEFERRED' : 'IMMEDIATE'
+ ];
+ } elseif ( $server['type'] === 'postgres' ) {
+ $server += [
+ 'port' => $mainConfig->get( 'DBport' ),
+ // Work around the reserved word usage in MediaWiki schema
+ 'keywordTableMap' => [ 'user' => 'mwuser', 'text' => 'pagecontent' ]
+ ];
+ } elseif ( $server['type'] === 'mssql' ) {
+ $server += [
+ 'port' => $mainConfig->get( 'DBport' ),
+ 'useWindowsAuth' => $mainConfig->get( 'DBWindowsAuthentication' )
+ ];
+ }
+
+ if ( in_array( $server['type'], self::getDbTypesWithSchemas(), true ) ) {
+ $server += [ 'schema' => $mainConfig->get( 'DBmwschema' ) ];
+ }
+
+ $flags = DBO_DEFAULT;
+ $flags |= $mainConfig->get( 'DebugDumpSql' ) ? DBO_DEBUG : 0;
+ if ( $server['type'] === 'oracle' ) {
+ $flags |= $mainConfig->get( 'DBOracleDRCP' ) ? DBO_PERSISTENT : 0;
+ }
+
+ $server += [
+ 'tablePrefix' => $mainConfig->get( 'DBprefix' ),
+ 'flags' => $flags,
+ 'sqlMode' => $mainConfig->get( 'SQLMode' ),
+ ];
+
+ return $server;
+ }
+
/**
* @param array $lbConf
* @param BagOStuff $sCache
* @param WANObjectCache $wCache
* @return array
*/
- private static function applyDefaultCaching(
+ private static function injectObjectCaches(
array $lbConf, BagOStuff $sCache, BagOStuff $mStash, WANObjectCache $wCache
) {
// Use APC/memcached style caching, but avoids loops with CACHE_DB (T141804)
* @param array $servers
* @param Config $mainConfig
*/
- private static function sanityCheckServerConfig( array $servers, Config $mainConfig ) {
+ private static function assertValidServerConfigs( array $servers, Config $mainConfig ) {
$ldDB = $mainConfig->get( 'DBname' ); // local domain DB
$ldTP = $mainConfig->get( 'DBprefix' ); // local domain prefix
self::purge( $this->urls );
if ( $wgCdnReboundPurgeDelay > 0 ) {
- JobQueueGroup::singleton()->lazyPush( new CdnPurgeJob(
- Title::makeTitle( NS_SPECIAL, 'Badtitle/' . __CLASS__ ),
- [
- 'urls' => $this->urls,
- 'jobReleaseTimestamp' => time() + $wgCdnReboundPurgeDelay
- ]
- ) );
+ JobQueueGroup::singleton()->lazyPush( new CdnPurgeJob( [
+ 'urls' => $this->urls,
+ 'jobReleaseTimestamp' => time() + $wgCdnReboundPurgeDelay
+ ] ) );
}
}
}
if ( $this->mType === 'date' ) {
+ $this->mParent->getOutput()->addModuleStyles( 'mediawiki.widgets.DateInputWidget.styles' );
return new MediaWiki\Widget\DateInputWidget( $params );
} else {
return new MediaWiki\Widget\DateTimeInputWidget( $params );
--- /dev/null
+<?php
+/**
+ * Interface for generic jobs only uses the parameters field.
+ *
+ * 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
+ */
+
+/**
+ * Interface for generic jobs only uses the parameters field and are JSON serializable
+ *
+ * @ingroup JobQueue
+ * @since 1.33
+ */
+interface GenericParameterJob extends IJobSpecification {
+ /**
+ * @param array $params JSON-serializable map of parameters
+ */
+ public function __construct( array $params );
+}
<?php
/**
- * Job queue task description base code.
+ * Job queue task description interface
*
* 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
*/
/**
- * Job queue task description interface
+ * Interface for serializable objects that describe a job queue task
+ *
+ * A job specification can be inserted into a queue via JobQueue::push().
+ * The specification parameters should be JSON serializable (e.g. no PHP classes).
+ * Whatever queue the job specification is pushed into is assumed to have job runners
+ * that will eventually pop the job specification from the queue, construct a RunnableJob
+ * instance from the specification, and then execute that instance via RunnableJob::run().
*
* @ingroup JobQueue
* @since 1.23
*/
interface IJobSpecification {
/**
- * @return string Job type
+ * @return string Job type that defines what sort of changes this job makes
*/
public function getType();
/**
- * @return array
+ * @return array Parameters that specify sources, targets, and options for execution
*/
public function getParams();
* @return bool Whether this is job is a root job
*/
public function isRootJob();
-
- /**
- * @return Title Descriptive title (this can simply be informative)
- */
- public function getTitle();
}
*
* @ingroup JobQueue
*/
-abstract class Job implements IJobSpecification {
+abstract class Job implements RunnableJob {
/** @var string */
public $command;
/** @var int Job must not be wrapped in the usual explicit LBFactory transaction round */
const JOB_NO_EXPLICIT_TRX_ROUND = 1;
- /**
- * Run the job
- * @return bool Success
- */
- abstract public function run();
-
/**
* Create the appropriate object to handle a specific job
*
$title = $params;
$params = func_num_args() >= 3 ? func_get_arg( 2 ) : [];
} else {
- // Subclasses can override getTitle() to return something more meaningful
- $title = Title::makeTitle( NS_SPECIAL, 'Blankpage' );
+ $title = ( isset( $params['namespace'] ) && isset( $params['title'] ) )
+ ? Title::makeTitle( $params['namespace'], $params['title'] )
+ : Title::makeTitle( NS_SPECIAL, '' );
}
+ $params = is_array( $params ) ? $params : []; // sanity
+
if ( isset( $wgJobClasses[$command] ) ) {
$handler = $wgJobClasses[$command];
if ( is_callable( $handler ) ) {
$job = call_user_func( $handler, $title, $params );
} elseif ( class_exists( $handler ) ) {
- $job = new $handler( $title, $params );
+ if ( is_subclass_of( $handler, GenericParameterJob::class ) ) {
+ $job = new $handler( $params );
+ } else {
+ $job = new $handler( $title, $params );
+ }
} else {
$job = null;
}
if ( $params instanceof Title ) {
// Backwards compatibility for old signature ($command, $title, $params)
$title = $params;
- $params = func_get_arg( 2 );
- } else {
- // Subclasses can override getTitle() to return something more meaningful
- $title = Title::makeTitle( NS_SPECIAL, 'Blankpage' );
+ $params = func_num_args() >= 3 ? func_get_arg( 2 ) : [];
+ $params = is_array( $params ) ? $params : []; // sanity
+ // Set namespace/title params if both are missing and this is not a dummy title
+ if (
+ $title->getDBkey() !== '' &&
+ !isset( $params['namespace'] ) &&
+ !isset( $params['title'] )
+ ) {
+ $params['namespace'] = $title->getNamespace();
+ $params['title'] = $title->getDBKey();
+ // Note that JobQueue classes will prefer the parameters over getTitle()
+ $this->title = $title;
+ }
}
$this->command = $command;
- $this->title = $title;
- $this->params = is_array( $params ) ? $params : [];
- if ( !isset( $this->params['requestId'] ) ) {
- $this->params['requestId'] = WebRequest::getRequestId();
+ $this->params = $params + [ 'requestId' => WebRequest::getRequestId() ];
+ if ( $this->title === null ) {
+ $this->title = ( isset( $params['namespace'] ) && isset( $params['title'] ) )
+ ? Title::makeTitle( $params['namespace'], $params['title'] )
+ : Title::makeTitle( NS_SPECIAL, '' );
}
}
/**
* @return Title
*/
- public function getTitle() {
+ final public function getTitle() {
return $this->title;
}
public function getDeduplicationInfo() {
$info = [
'type' => $this->getType(),
- 'namespace' => $this->getTitle()->getNamespace(),
- 'title' => $this->getTitle()->getDBkey(),
'params' => $this->getParams()
];
if ( is_array( $info['params'] ) ) {
* Outside callers should use JobQueueGroup::pop() instead of this function.
*
* @throws JobQueueError
- * @return Job|bool Returns false if there are no jobs
+ * @return RunnableJob|bool Returns false if there are no jobs
*/
final public function pop() {
$this->assertNotReadOnly();
/**
* @see JobQueue::pop()
- * @return Job|bool
+ * @return RunnableJob|bool
*/
abstract protected function doPop();
* This does nothing for certain queue classes or if "claimTTL" is not set.
* Outside callers should use JobQueueGroup::ack() instead of this function.
*
- * @param Job $job
+ * @param RunnableJob $job
* @return void
* @throws JobQueueError
*/
- final public function ack( Job $job ) {
+ final public function ack( RunnableJob $job ) {
$this->assertNotReadOnly();
if ( $job->getType() !== $this->type ) {
throw new JobQueueError( "Got '{$job->getType()}' job; expected '{$this->type}'." );
/**
* @see JobQueue::ack()
- * @param Job $job
+ * @param RunnableJob $job
*/
- abstract protected function doAck( Job $job );
+ abstract protected function doAck( RunnableJob $job );
/**
* Register the "root job" of a given job into the queue for de-duplication.
/**
* Check if the "root" job of a given job has been superseded by a newer one
*
- * @param Job $job
+ * @param IJobSpecification $job
* @throws JobQueueError
* @return bool
*/
- final protected function isRootJobOldDuplicate( Job $job ) {
+ final protected function isRootJobOldDuplicate( IJobSpecification $job ) {
if ( $job->getType() !== $this->type ) {
throw new JobQueueError( "Got '{$job->getType()}' job; expected '{$this->type}'." );
}
/**
* @see JobQueue::isRootJobOldDuplicate()
- * @param Job $job
+ * @param IJobSpecification $job
* @return bool
*/
- protected function doIsRootJobOldDuplicate( Job $job ) {
+ protected function doIsRootJobOldDuplicate( IJobSpecification $job ) {
if ( !$job->hasRootJobParams() ) {
return false; // job has no de-deplication info
}
return null; // not supported
}
+ /**
+ * @param string $command
+ * @param array $params
+ * @return Job
+ */
+ protected function factoryJob( $command, $params ) {
+ // @TODO: dependency inject this as a callback
+ return Job::factory( $command, $params );
+ }
+
/**
* @throws JobQueueReadOnlyError
*/
/**
* @see JobQueue::doPop()
- * @return Job|bool
+ * @return RunnableJob|bool
*/
protected function doPop() {
$dbw = $this->getMasterDB();
break; // nothing to do
}
$this->incrStats( 'pops', $this->type );
+
// Get the job object from the row...
- $title = Title::makeTitle( $row->job_namespace, $row->job_title );
- $job = Job::factory( $row->job_cmd, $title,
- self::extractBlob( $row->job_params ) );
+ $params = self::extractBlob( $row->job_params );
+ $params = is_array( $params ) ? $params : []; // sanity
+ $params += [ 'namespace' => $row->job_namespace, 'title' => $row->job_title ];
+ $job = $this->factoryJob( $row->job_cmd, $params );
$job->setMetadata( 'id', $row->job_id );
$job->setMetadata( 'timestamp', $row->job_timestamp );
break; // done
/**
* @see JobQueue::doAck()
- * @param Job $job
+ * @param RunnableJob $job
* @throws MWException
*/
- protected function doAck( Job $job ) {
+ protected function doAck( RunnableJob $job ) {
$id = $job->getMetadata( 'id' );
if ( $id === null ) {
throw new MWException( "Job of type '{$job->getType()}' has no ID." );
return new MappedIterator(
$dbr->select( 'job', self::selectFields(), $conds ),
function ( $row ) {
- $job = Job::factory(
- $row->job_cmd,
- Title::makeTitle( $row->job_namespace, $row->job_title ),
- strlen( $row->job_params ) ? unserialize( $row->job_params ) : []
- );
+ $params = strlen( $row->job_params ) ? unserialize( $row->job_params ) : [];
+ $params = is_array( $params ) ? $params : []; // sanity
+ $params += [
+ 'namespace' => $row->job_namespace,
+ 'title' => $row->job_title
+ ];
+
+ $job = $this->factoryJob( $row->job_cmd, $params );
$job->setMetadata( 'id', $row->job_id );
$job->setMetadata( 'timestamp', $row->job_timestamp );
return [
// Fields that describe the nature of the job
'job_cmd' => $job->getType(),
- 'job_namespace' => $job->getTitle()->getNamespace(),
- 'job_title' => $job->getTitle()->getDBkey(),
+ 'job_namespace' => $job->getParams()['namespace'] ?? NS_SPECIAL,
+ 'job_title' => $job->getParams()['title'] ?? '',
'job_params' => self::makeBlob( $job->getParams() ),
// Additional job metadata
'job_timestamp' => $db->timestamp(),
* @param HashRing &$partitionRing
* @param int $flags
* @throws JobQueueError
- * @return array List of Job object that could not be inserted
+ * @return IJobSpecification[] List of Job object that could not be inserted
*/
protected function tryJobInsertions( array $jobs, HashRing &$partitionRing, $flags ) {
$jobsLeft = [];
return false;
}
- protected function doAck( Job $job ) {
+ protected function doAck( RunnableJob $job ) {
$partition = $job->getMetadata( 'QueuePartition' );
if ( $partition === null ) {
throw new MWException( "The given job has no defined partition name." );
$this->partitionQueues[$partition]->ack( $job );
}
- protected function doIsRootJobOldDuplicate( Job $job ) {
+ protected function doIsRootJobOldDuplicate( IJobSpecification $job ) {
$signature = $job->getRootJobParams()['rootJobSignature'];
$partition = $this->partitionRing->getLiveLocation( $signature );
try {
* @param int|string $qtype JobQueueGroup::TYPE_* constant or job type string
* @param int $flags Bitfield of JobQueueGroup::USE_* constants
* @param array $blacklist List of job types to ignore
- * @return Job|bool Returns false on failure
+ * @return RunnableJob|bool Returns false on failure
*/
public function pop( $qtype = self::TYPE_DEFAULT, $flags = 0, array $blacklist = [] ) {
global $wgJobClasses;
/**
* @see JobQueue::doPop
*
- * @return Job|bool
+ * @return RunnableJob|bool
*/
protected function doPop() {
if ( $this->doGetSize() == 0 ) {
/**
* @see JobQueue::doAck
*
- * @param Job $job
+ * @param RunnableJob $job
*/
- protected function doAck( Job $job ) {
+ protected function doAck( RunnableJob $job ) {
if ( $this->getAcquiredCount() == 0 ) {
return;
}
/**
* @param IJobSpecification $spec
- *
- * @return Job
+ * @return RunnableJob
*/
public function jobFromSpecInternal( IJobSpecification $spec ) {
- return Job::factory( $spec->getType(), $spec->getTitle(), $spec->getParams() );
+ return $this->factoryJob( $spec->getType(), $spec->getParams() );
}
/**
/**
* @see JobQueue::doPop()
- * @return Job|bool
+ * @return RunnableJob|bool
* @throws JobQueueError
*/
protected function doPop() {
/**
* @see JobQueue::doAck()
- * @param Job $job
- * @return Job|bool
+ * @param RunnableJob $job
+ * @return RunnableJob|bool
* @throws UnexpectedValueException
* @throws JobQueueError
*/
- protected function doAck( Job $job ) {
+ protected function doAck( RunnableJob $job ) {
$uuid = $job->getMetadata( 'uuid' );
if ( $uuid === null ) {
throw new UnexpectedValueException( "Job of type '{$job->getType()}' has no UUID." );
/**
* @see JobQueue::doIsRootJobOldDuplicate()
- * @param Job $job
+ * @param IJobSpecification $job
* @return bool
* @throws JobQueueError
*/
- protected function doIsRootJobOldDuplicate( Job $job ) {
+ protected function doIsRootJobOldDuplicate( IJobSpecification $job ) {
if ( !$job->hasRootJobParams() ) {
return false; // job has no de-deplication info
}
*
* @param string $uid
* @param RedisConnRef $conn
- * @return Job|bool Returns false if the job does not exist
+ * @return RunnableJob|bool Returns false if the job does not exist
* @throws JobQueueError
* @throws UnexpectedValueException
*/
if ( !is_array( $item ) ) { // this shouldn't happen
throw new UnexpectedValueException( "Could not find job with ID '$uid'." );
}
- $title = Title::makeTitle( $item['namespace'], $item['title'] );
- $job = Job::factory( $item['type'], $title, $item['params'] );
+
+ $params = $item['params'];
+ $params += [ 'namespace' => $item['namespace'], 'title' => $item['title'] ];
+ $job = $this->factoryJob( $item['type'], $params );
$job->setMetadata( 'uuid', $item['uuid'] );
$job->setMetadata( 'timestamp', $item['timestamp'] );
// Add in attempt count for debugging at showJobs.php
return [
// Fields that describe the nature of the job
'type' => $job->getType(),
- 'namespace' => $job->getTitle()->getNamespace(),
- 'title' => $job->getTitle()->getDBkey(),
+ 'namespace' => $job->getParams()['namespace'] ?? NS_SPECIAL,
+ 'title' => $job->getParams()['title'] ?? '',
'params' => $job->getParams(),
// Some jobs cannot run until a "release timestamp"
'rtimestamp' => $job->getReleaseTimestamp() ?: 0,
/**
* @param array $fields
- * @return Job|bool
+ * @return RunnableJob|bool
*/
protected function getJobFromFields( array $fields ) {
- $title = Title::makeTitle( $fields['namespace'], $fields['title'] );
- $job = Job::factory( $fields['type'], $title, $fields['params'] );
+ $params = $fields['params'];
+ $params += [ 'namespace' => $fields['namespace'], 'title' => $fields['title'] ];
+
+ $job = $this->factoryJob( $fields['type'], $params );
$job->setMetadata( 'uuid', $fields['uuid'] );
$job->setMetadata( 'timestamp', $fields['timestamp'] );
* $job = new JobSpecification(
* 'null',
* array( 'lives' => 1, 'usleep' => 100, 'pi' => 3.141569 ),
- * array( 'removeDuplicates' => 1 ),
- * Title::makeTitle( NS_SPECIAL, 'nullity' )
+ * array( 'removeDuplicates' => 1 )
* );
* JobQueueGroup::singleton()->push( $job )
* @endcode
$this->validateParams( $opts );
$this->type = $type;
+ if ( $title instanceof Title ) {
+ // Make sure JobQueue classes can pull the title from parameters alone
+ if ( $title->getDBkey() !== '' ) {
+ $params += [
+ 'namespace' => $title->getNamespace(),
+ 'title' => $title->getDBkey()
+ ];
+ }
+ } else {
+ $title = Title::makeTitle( NS_SPECIAL, '' );
+ }
$this->params = $params;
- $this->title = $title ?: Title::makeTitle( NS_SPECIAL, 'Blankpage' );
+ $this->title = $title;
$this->opts = $opts;
}
--- /dev/null
+<?php
+/**
+ * Job queue task instance that can be executed via a run() method
+ *
+ * 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
+ */
+
+/**
+ * Job that has a run() method and metadata accessors for JobQueue::pop() and JobQueue::ack()
+ *
+ * Instances are not only enqueueable via JobQueue::push(), but they can also be executed by
+ * by calling their run() method. When constructing a job to be enqueued via JobQueue::push(),
+ * it will not be possible to construct a RunnableJob instance if the class for that job is not
+ * loaded by the application for the local DB domain. In that case, the general-purpose
+ * JobSpecification class can be used instead.
+ *
+ * @ingroup JobQueue
+ * @since 1.33
+ */
+interface RunnableJob extends IJobSpecification {
+ /**
+ * Run the job
+ * @return bool Success
+ */
+ public function run();
+
+ /**
+ * @param string|null $field Metadata field or null to get all the metadata
+ * @return mixed|null Value; null if missing
+ */
+ public function getMetadata( $field = null );
+
+ /**
+ * @param string $field Key name to set the value for
+ * @param mixed $value The value to set the field for
+ * @return mixed|null The prior field value; null if missing
+ */
+ public function setMetadata( $field, $value );
+}
* @ingroup JobQueue
* @since 1.27
*/
-class CdnPurgeJob extends Job {
- /**
- * @param Title $title
- * @param array $params Job parameters (urls)
- */
- function __construct( Title $title, array $params ) {
- parent::__construct( 'cdnPurge', $title, $params );
+class CdnPurgeJob extends Job implements GenericParameterJob {
+ function __construct( array $params ) {
+ parent::__construct( 'cdnPurge', $params );
$this->removeDuplicates = false; // delay semantics are critical
}
* @ingroup JobQueue
* @since 1.31
*/
-class ClearUserWatchlistJob extends Job {
+class ClearUserWatchlistJob extends Job implements GenericParameterJob {
+ /**
+ * @param array $params
+ * - userId, The ID for the user whose watchlist is being cleared.
+ * - maxWatchlistId, The maximum wl_id at the time the job was first created,
+ */
+ public function __construct( array $params ) {
+ parent::__construct( 'clearUserWatchlist', $params );
+
+ $this->removeDuplicates = true;
+ }
/**
* @param User $user User to clear the watchlist for.
* @return ClearUserWatchlistJob
*/
public static function newForUser( User $user, $maxWatchlistId ) {
- return new self(
- null,
- [ 'userId' => $user->getId(), 'maxWatchlistId' => $maxWatchlistId ]
- );
- }
-
- /**
- * @param Title|null $title Not used by this job.
- * @param array $params
- * - userId, The ID for the user whose watchlist is being cleared.
- * - maxWatchlistId, The maximum wl_id at the time the job was first created,
- */
- public function __construct( Title $title = null, array $params ) {
- parent::__construct(
- 'clearUserWatchlist',
- SpecialPage::getTitleFor( 'EditWatchlist', 'clear' ),
- $params
- );
-
- $this->removeDuplicates = true;
+ return new self( [ 'userId' => $user->getId(), 'maxWatchlistId' => $maxWatchlistId ] );
}
public function run() {
if ( count( $watchlistIds ) === (int)$batchSize ) {
// Until we get less results than the limit, recursively push
// the same job again.
- JobQueueGroup::singleton()->push( new self( $this->getTitle(), $this->getParams() ) );
+ JobQueueGroup::singleton()->push( new self( $this->getParams() ) );
}
return true;
* @ingroup JobQueue
* @since 1.31
*/
-class ClearWatchlistNotificationsJob extends Job {
- function __construct( Title $title, array $params ) {
- parent::__construct( 'clearWatchlistNotifications', $title, $params );
+class ClearWatchlistNotificationsJob extends Job implements GenericParameterJob {
+ function __construct( array $params ) {
+ parent::__construct( 'clearWatchlistNotifications', $params );
static $required = [ 'userId', 'casTime' ];
$missing = implode( ', ', array_diff( $required, array_keys( $this->params ) ) );
/**
* Class DeletePageJob
*/
-class DeletePageJob extends Job {
- public function __construct( $title, $params = [] ) {
- parent::__construct( 'deletePage', $title, $params );
+class DeletePageJob extends Job implements GenericParameterJob {
+ public function __construct( array $params ) {
+ parent::__construct( 'deletePage', $params );
+
+ $this->title = Title::makeTitle( $params['namespace'], $params['title'] );
}
- /**
- * Execute the job
- *
- * @return bool
- */
public function run() {
// Failure to load the page is not job failure.
// A parallel deletion operation may have already completed the page deletion.
*
* @ingroup JobQueue
*/
-final class DuplicateJob extends Job {
+final class DuplicateJob extends Job implements GenericParameterJob {
/**
* Callers should use DuplicateJob::newFromJob() instead
*
- * @param Title $title
* @param array $params Job parameters
*/
- function __construct( Title $title, array $params ) {
- parent::__construct( 'duplicate', $title, $params );
+ function __construct( array $params ) {
+ parent::__construct( 'duplicate', $params );
}
/**
* Get a duplicate no-op version of a job
*
- * @param Job $job
+ * @param RunnableJob $job
* @return Job
*/
- public static function newFromJob( Job $job ) {
- $djob = new self( $job->getTitle(), $job->getParams() );
+ public static function newFromJob( RunnableJob $job ) {
+ $djob = new self( $job->getParams() );
$djob->command = $job->getType();
$djob->params = is_array( $djob->params ) ? $djob->params : [];
$djob->params = [ 'isDuplicate' => true ] + $djob->params;
- $djob->metadata = $job->metadata;
+ $djob->metadata = $job->getMetadata();
return $djob;
}
* @ingroup JobQueue
* @since 1.25
*/
-final class EnqueueJob extends Job {
+final class EnqueueJob extends Job implements GenericParameterJob {
/**
* Callers should use the factory methods instead
*
- * @param Title $title
* @param array $params Job parameters
*/
- function __construct( Title $title, array $params ) {
- parent::__construct( 'enqueue', $title, $params );
+ public function __construct( array $params ) {
+ parent::__construct( 'enqueue', $params );
}
/**
}
}
- $eJob = new self(
- Title::makeTitle( NS_SPECIAL, 'Badtitle/' . __CLASS__ ),
- [ 'jobsByDomain' => $jobMapsByDomain ]
- );
+ $eJob = new self( [ 'jobsByDomain' => $jobMapsByDomain ] );
// If *all* jobs to be pushed are to be de-duplicated (a common case), then
// de-duplicate this whole job itself to avoid build up in high traffic cases
$eJob->removeDuplicates = $deduplicate;
* @code
* $ php maintenance/eval.php
* > $queue = JobQueueGroup::singleton();
- * > $job = new NullJob( Title::newMainPage(), [ 'lives' => 10 ] );
+ * > $job = new NullJob( [ 'lives' => 10 ] );
* > $queue->push( $job );
* @endcode
* You can then confirm the job has been enqueued by using the showJobs.php
*
* @ingroup JobQueue
*/
-class NullJob extends Job {
+class NullJob extends Job implements GenericParameterJob {
/**
- * @param Title $title
* @param array $params Job parameters (lives, usleep)
*/
- function __construct( Title $title, array $params ) {
- parent::__construct( 'null', $title, $params );
+ function __construct( array $params ) {
+ parent::__construct( 'null', $params );
if ( !isset( $this->params['lives'] ) ) {
$this->params['lives'] = 1;
}
if ( $this->params['lives'] > 1 ) {
$params = $this->params;
$params['lives']--;
- $job = new self( $this->title, $params );
+ $job = new self( $params );
JobQueueGroup::singleton()->push( $job );
}
* @ingroup JobQueue
*/
-class UserGroupExpiryJob extends Job {
- public function __construct( $params = [] ) {
- parent::__construct( 'userGroupExpiry', Title::newMainPage(), $params );
+class UserGroupExpiryJob extends Job implements GenericParameterJob {
+ public function __construct( array $params = [] ) {
+ parent::__construct( 'userGroupExpiry', $params );
$this->removeDuplicates = true;
}
$dbw->endAtomic( __METHOD__ );
$jobParams = [
+ 'namespace' => $this->getTitle()->getNamespace(),
+ 'title' => $this->getTitle()->getDBkey(),
'wikiPageId' => $id,
'requestId' => $webRequestId ?? WebRequest::getRequestId(),
'reason' => $reason,
'logsubtype' => $logsubtype,
];
- $job = new DeletePageJob( $this->getTitle(), $jobParams );
+ $job = new DeletePageJob( $jobParams );
JobQueueGroup::singleton()->push( $job );
$status->warning( 'delete-scheduled',
*/
public static function formatDate( $parser, $date, $defaultPref = null ) {
$lang = $parser->getFunctionLang();
- $df = DateFormatter::getInstance( $lang );
+ $df = MediaWikiServices::getInstance()->getDateFormatterFactory()->get( $lang );
$date = trim( $date );
use MediaWiki\MediaWikiServices;
/**
- * Date formatter, recognises dates in plain text and formats them according to user preferences.
- * @todo preferences, OutputPage
+ * Date formatter. Recognises dates and formats them according to a specified preference.
+ *
+ * This class was originally introduced to detect and transform dates in free text. It is now
+ * only used by the {{#dateformat}} parser function. This is a very rudimentary date formatter;
+ * Language::sprintfDate() has many more features and is the correct choice for most new code.
+ * The main advantage of this date formatter is that it is able to format incomplete dates with an
+ * unspecified year.
+ *
* @ingroup Parser
*/
class DateFormatter {
- private $mSource, $mTarget;
- private $monthNames = '';
-
+ /** @var string[] Date format regexes indexed the class constants */
private $regexes;
- private $rules, $xMonths, $preferences;
- private $lang, $mLinked;
+ /**
+ * @var int[][] Array of special rules. The first key is the preference ID
+ * (one of the class constants), the second key is the detected source
+ * format, and the value is the ID of the target format that will be used
+ * in that case.
+ */
+ private $rules = [];
- /** @var string[] */
- private $keys;
+ /**
+ * @var int[] Month numbers by lowercase name
+ */
+ private $xMonths = [];
- /** @var string[] */
- private $targets;
+ /**
+ * @var string[] Month names by number
+ */
+ private $monthNames = [];
+ /**
+ * @var int[] A map of descriptive preference text to internal format ID
+ */
+ private $preferenceIDs;
+
+ /** @var string[] Format strings similar to those used by date(), indexed by ID */
+ private $targetFormats;
+
+ /** Used as a preference ID for rules that apply regardless of preference */
const ALL = -1;
+
+ /** No preference: the date may be left in the same format as the input */
const NONE = 0;
+
+ /** e.g. January 15, 2001 */
const MDY = 1;
+
+ /** e.g. 15 January 2001 */
const DMY = 2;
+
+ /** e.g. 2001 January 15 */
const YMD = 3;
- const ISO1 = 4;
+
+ /** e.g. 2001-01-15 */
+ const ISO = 4;
+
+ /** The highest ID that is a valid user preference */
const LASTPREF = 4;
- const ISO2 = 5;
- const YDM = 6;
- const DM = 7;
- const MD = 8;
- const LAST = 8;
+
+ /** e.g. 2001, 15 January */
+ const YDM = 5;
+
+ /** e.g. 15 January */
+ const DM = 6;
+
+ /** e.g. January 15 */
+ const MD = 7;
+
+ /** The highest ID that is a valid target format */
+ const LAST = 7;
/**
* @param Language $lang In which language to format the date
*/
public function __construct( Language $lang ) {
- $this->lang = $lang;
-
- $this->monthNames = $this->getMonthRegex();
+ $monthRegexParts = [];
for ( $i = 1; $i <= 12; $i++ ) {
- $this->xMonths[$this->lang->lc( $this->lang->getMonthName( $i ) )] = $i;
- $this->xMonths[$this->lang->lc( $this->lang->getMonthAbbreviation( $i ) )] = $i;
+ $monthName = $lang->getMonthName( $i );
+ $monthAbbrev = $lang->getMonthAbbreviation( $i );
+ $this->monthNames[$i] = $monthName;
+ $monthRegexParts[] = preg_quote( $monthName, '/' );
+ $monthRegexParts[] = preg_quote( $monthAbbrev, '/' );
+ $this->xMonths[mb_strtolower( $monthName )] = $i;
+ $this->xMonths[mb_strtolower( $monthAbbrev )] = $i;
}
- $this->regexTrail = '(?![a-z])/iu';
-
- # Partial regular expressions
- $this->prxDM = '\[\[(\d{1,2})[ _](' . $this->monthNames . ')\]\]';
- $this->prxMD = '\[\[(' . $this->monthNames . ')[ _](\d{1,2})\]\]';
- $this->prxY = '\[\[(\d{1,4}([ _]BC|))\]\]';
- $this->prxISO1 = '\[\[(-?\d{4})]]-\[\[(\d{2})-(\d{2})\]\]';
- $this->prxISO2 = '\[\[(-?\d{4})-(\d{2})-(\d{2})\]\]';
-
- # Real regular expressions
- $this->regexes[self::DMY] = "/{$this->prxDM}(?: *, *| +){$this->prxY}{$this->regexTrail}";
- $this->regexes[self::YDM] = "/{$this->prxY}(?: *, *| +){$this->prxDM}{$this->regexTrail}";
- $this->regexes[self::MDY] = "/{$this->prxMD}(?: *, *| +){$this->prxY}{$this->regexTrail}";
- $this->regexes[self::YMD] = "/{$this->prxY}(?: *, *| +){$this->prxMD}{$this->regexTrail}";
- $this->regexes[self::DM] = "/{$this->prxDM}{$this->regexTrail}";
- $this->regexes[self::MD] = "/{$this->prxMD}{$this->regexTrail}";
- $this->regexes[self::ISO1] = "/{$this->prxISO1}{$this->regexTrail}";
- $this->regexes[self::ISO2] = "/{$this->prxISO2}{$this->regexTrail}";
-
- # Extraction keys
- # See the comments in replace() for the meaning of the letters
- $this->keys[self::DMY] = 'jFY';
- $this->keys[self::YDM] = 'Y jF';
- $this->keys[self::MDY] = 'FjY';
- $this->keys[self::YMD] = 'Y Fj';
- $this->keys[self::DM] = 'jF';
- $this->keys[self::MD] = 'Fj';
- $this->keys[self::ISO1] = 'ymd'; # y means ISO year
- $this->keys[self::ISO2] = 'ymd';
-
- # Target date formats
- $this->targets[self::DMY] = '[[F j|j F]] [[Y]]';
- $this->targets[self::YDM] = '[[Y]], [[F j|j F]]';
- $this->targets[self::MDY] = '[[F j]], [[Y]]';
- $this->targets[self::YMD] = '[[Y]] [[F j]]';
- $this->targets[self::DM] = '[[F j|j F]]';
- $this->targets[self::MD] = '[[F j]]';
- $this->targets[self::ISO1] = '[[Y|y]]-[[F j|m-d]]';
- $this->targets[self::ISO2] = '[[y-m-d]]';
-
- # Rules
- # pref source target
+ // Partial regular expressions
+ $monthNames = implode( '|', $monthRegexParts );
+ $dm = "(?<day>\d{1,2})[ _](?<monthName>{$monthNames})";
+ $md = "(?<monthName>{$monthNames})[ _](?<day>\d{1,2})";
+ $y = '(?<year>\d{1,4}([ _]BC|))';
+ $iso = '(?<isoYear>-?\d{4})-(?<isoMonth>\d{2})-(?<isoDay>\d{2})';
+
+ $this->regexes = [
+ self::DMY => "/^{$dm}(?: *, *| +){$y}$/iu",
+ self::YDM => "/^{$y}(?: *, *| +){$dm}$/iu",
+ self::MDY => "/^{$md}(?: *, *| +){$y}$/iu",
+ self::YMD => "/^{$y}(?: *, *| +){$md}$/iu",
+ self::DM => "/^{$dm}$/iu",
+ self::MD => "/^{$md}$/iu",
+ self::ISO => "/^{$iso}$/iu",
+ ];
+
+ // Target date formats
+ $this->targetFormats = [
+ self::DMY => 'j F Y',
+ self::YDM => 'Y, j F',
+ self::MDY => 'F j, Y',
+ self::YMD => 'Y F j',
+ self::DM => 'j F',
+ self::MD => 'F j',
+ self::ISO => 'y-m-d',
+ ];
+
+ // Rules
+ // pref source target
$this->rules[self::DMY][self::MD] = self::DM;
$this->rules[self::ALL][self::MD] = self::MD;
$this->rules[self::MDY][self::DM] = self::MD;
$this->rules[self::ALL][self::DM] = self::DM;
- $this->rules[self::NONE][self::ISO2] = self::ISO1;
+ $this->rules[self::NONE][self::ISO] = self::ISO;
- $this->preferences = [
+ $this->preferenceIDs = [
'default' => self::NONE,
'dmy' => self::DMY,
'mdy' => self::MDY,
'ymd' => self::YMD,
- 'ISO 8601' => self::ISO1,
+ 'ISO 8601' => self::ISO,
];
}
/**
* Get a DateFormatter object
*
+ * @deprecated since 1.33 use MediaWikiServices::getDateFormatterFactory()
+ *
* @param Language|null $lang In which language to format the date
* Defaults to the site content language
* @return DateFormatter
*/
public static function getInstance( Language $lang = null ) {
- global $wgMainCacheType;
-
$lang = $lang ?? MediaWikiServices::getInstance()->getContentLanguage();
- $cache = ObjectCache::getLocalServerInstance( $wgMainCacheType );
-
- static $dateFormatter = false;
- if ( !$dateFormatter ) {
- $dateFormatter = $cache->getWithSetCallback(
- $cache->makeKey( 'dateformatter', $lang->getCode() ),
- $cache::TTL_HOUR,
- function () use ( $lang ) {
- return new DateFormatter( $lang );
- }
- );
- }
-
- return $dateFormatter;
+ return MediaWikiServices::getInstance()->getDateFormatterFactory()->get( $lang );
}
/**
- * @param string $preference User preference
+ * @param string $preference User preference, must be one of "default",
+ * "dmy", "mdy", "ymd" or "ISO 8601".
* @param string $text Text to reformat
- * @param array $options Array can contain 'linked' and/or 'match-whole'
+ * @param array $options Ignored. Since 1.33, 'match-whole' is implied, and
+ * 'linked' has been removed.
*
* @return string
*/
- public function reformat( $preference, $text, $options = [ 'linked' ] ) {
- $linked = in_array( 'linked', $options );
- $match_whole = in_array( 'match-whole', $options );
-
- if ( isset( $this->preferences[$preference] ) ) {
- $preference = $this->preferences[$preference];
+ public function reformat( $preference, $text, $options = [] ) {
+ if ( isset( $this->preferenceIDs[$preference] ) ) {
+ $preference = $this->preferenceIDs[$preference];
} else {
$preference = self::NONE;
}
- for ( $i = 1; $i <= self::LAST; $i++ ) {
- $this->mSource = $i;
- if ( isset( $this->rules[$preference][$i] ) ) {
+ for ( $source = 1; $source <= self::LAST; $source++ ) {
+ if ( isset( $this->rules[$preference][$source] ) ) {
# Specific rules
- $this->mTarget = $this->rules[$preference][$i];
- } elseif ( isset( $this->rules[self::ALL][$i] ) ) {
+ $target = $this->rules[$preference][$source];
+ } elseif ( isset( $this->rules[self::ALL][$source] ) ) {
# General rules
- $this->mTarget = $this->rules[self::ALL][$i];
+ $target = $this->rules[self::ALL][$source];
} elseif ( $preference ) {
# User preference
- $this->mTarget = $preference;
+ $target = $preference;
} else {
# Default
- $this->mTarget = $i;
+ $target = $source;
}
- $regex = $this->regexes[$i];
+ $regex = $this->regexes[$source];
- // Horrible hack
- if ( !$linked ) {
- $regex = str_replace( [ '\[\[', '\]\]' ], '', $regex );
- }
-
- if ( $match_whole ) {
- // Let's hope this works
- $regex = preg_replace( '!^/!', '/^', $regex );
- $regex = str_replace( $this->regexTrail,
- '$' . $this->regexTrail, $regex );
- }
+ $text = preg_replace_callback( $regex,
+ function ( $match ) use ( $target ) {
+ $format = $this->targetFormats[$target];
- // Another horrible hack
- $this->mLinked = $linked;
- $text = preg_replace_callback( $regex, [ $this, 'replace' ], $text );
- unset( $this->mLinked );
- }
- return $text;
- }
+ $text = '';
- /**
- * Regexp replacement callback
- *
- * @param array $matches
- * @return string
- */
- private function replace( $matches ) {
- # Extract information from $matches
- $linked = $this->mLinked ?? true;
-
- $bits = [];
- $key = $this->keys[$this->mSource];
- $keyLength = strlen( $key );
- for ( $p = 0; $p < $keyLength; $p++ ) {
- if ( $key[$p] != ' ' ) {
- $bits[$key[$p]] = $matches[$p + 1];
- }
- }
-
- return $this->formatDate( $bits, $matches[0], $linked );
- }
-
- /**
- * @param array $bits
- * @param string $orig Original input string, to be returned
- * on formatting failure.
- * @param bool $link
- * @return string
- */
- private function formatDate( $bits, $orig, $link = true ) {
- $format = $this->targets[$this->mTarget];
-
- if ( !$link ) {
- // strip piped links
- $format = preg_replace( '/\[\[[^|]+\|([^\]]+)\]\]/', '$1', $format );
- // strip remaining links
- $format = str_replace( [ '[[', ']]' ], '', $format );
- }
-
- # Construct new date
- $text = '';
- $fail = false;
-
- // Pre-generate y/Y stuff because we need the year for the <span> title.
- if ( !isset( $bits['y'] ) && isset( $bits['Y'] ) ) {
- $bits['y'] = $this->makeIsoYear( $bits['Y'] );
- }
- if ( !isset( $bits['Y'] ) && isset( $bits['y'] ) ) {
- $bits['Y'] = $this->makeNormalYear( $bits['y'] );
- }
-
- if ( !isset( $bits['m'] ) ) {
- $m = $this->makeIsoMonth( $bits['F'] );
- if ( $m === false ) {
- $fail = true;
- } else {
- $bits['m'] = $m;
- }
- }
-
- if ( !isset( $bits['d'] ) ) {
- $bits['d'] = sprintf( '%02d', $bits['j'] );
- }
-
- $formatLength = strlen( $format );
- for ( $p = 0; $p < $formatLength; $p++ ) {
- $char = $format[$p];
- switch ( $char ) {
- case 'd': # ISO day of month
- $text .= $bits['d'];
- break;
- case 'm': # ISO month
- $text .= $bits['m'];
- break;
- case 'y': # ISO year
- $text .= $bits['y'];
- break;
- case 'j': # ordinary day of month
- if ( !isset( $bits['j'] ) ) {
- $text .= intval( $bits['d'] );
- } else {
- $text .= $bits['j'];
+ // Pre-generate y/Y stuff because we need the year for the <span> title.
+ if ( !isset( $match['isoYear'] ) && isset( $match['year'] ) ) {
+ $match['isoYear'] = $this->makeIsoYear( $match['year'] );
+ }
+ if ( !isset( $match['year'] ) && isset( $match['isoYear'] ) ) {
+ $match['year'] = $this->makeNormalYear( $match['isoYear'] );
}
- break;
- case 'F': # long month
- if ( !isset( $bits['F'] ) ) {
- $m = intval( $bits['m'] );
- if ( $m > 12 || $m < 1 ) {
- $fail = true;
+
+ if ( !isset( $match['isoMonth'] ) ) {
+ $m = $this->makeIsoMonth( $match['monthName'] );
+ if ( $m === false ) {
+ // Fail
+ return $match[0];
} else {
- $text .= $this->lang->getMonthName( $m );
+ $match['isoMonth'] = $m;
}
- } else {
- $text .= ucfirst( $bits['F'] );
}
- break;
- case 'Y': # ordinary (optional BC) year
- $text .= $bits['Y'];
- break;
- default:
- $text .= $char;
- }
- }
- if ( $fail ) {
- // This occurs when parsing a date with day or month outside the bounds
- // of possibilities.
- return $orig;
- }
- $isoBits = [];
- if ( isset( $bits['y'] ) ) {
- $isoBits[] = $bits['y'];
- }
- $isoBits[] = $bits['m'];
- $isoBits[] = $bits['d'];
- $isoDate = implode( '-', $isoBits );
+ if ( !isset( $match['isoDay'] ) ) {
+ $match['isoDay'] = sprintf( '%02d', $match['day'] );
+ }
+
+ $formatLength = strlen( $format );
+ for ( $p = 0; $p < $formatLength; $p++ ) {
+ $char = $format[$p];
+ switch ( $char ) {
+ case 'd': // ISO day of month
+ $text .= $match['isoDay'];
+ break;
+ case 'm': // ISO month
+ $text .= $match['isoMonth'];
+ break;
+ case 'y': // ISO year
+ $text .= $match['isoYear'];
+ break;
+ case 'j': // ordinary day of month
+ if ( !isset( $match['day'] ) ) {
+ $text .= intval( $match['isoDay'] );
+ } else {
+ $text .= $match['day'];
+ }
+ break;
+ case 'F': // long month
+ $m = intval( $match['isoMonth'] );
+ if ( $m > 12 || $m < 1 ) {
+ // Fail
+ return $match[0];
+ } else {
+ $text .= $this->monthNames[$m];
+ }
+ break;
+ case 'Y': // ordinary (optional BC) year
+ $text .= $match['year'];
+ break;
+ default:
+ $text .= $char;
+ }
+ }
- // Output is not strictly HTML (it's wikitext), but <span> is whitelisted.
- $text = Html::rawElement( 'span',
- [ 'class' => 'mw-formatted-date', 'title' => $isoDate ], $text );
+ $isoBits = [];
+ if ( isset( $match['isoYear'] ) ) {
+ $isoBits[] = $match['isoYear'];
+ }
+ $isoBits[] = $match['isoMonth'];
+ $isoBits[] = $match['isoDay'];
+ $isoDate = implode( '-', $isoBits );
- return $text;
- }
+ // Output is not strictly HTML (it's wikitext), but <span> is whitelisted.
+ $text = Html::rawElement( 'span',
+ [ 'class' => 'mw-formatted-date', 'title' => $isoDate ], $text );
- /**
- * Return a regex that can be used to find month names in string
- * @return string regex to find the months with
- */
- private function getMonthRegex() {
- $names = [];
- for ( $i = 1; $i <= 12; $i++ ) {
- $names[] = preg_quote( $this->lang->getMonthName( $i ), '/' );
- $names[] = preg_quote( $this->lang->getMonthAbbreviation( $i ), '/' );
+ return $text;
+ }, $text
+ );
}
- return implode( '|', $names );
+ return $text;
}
/**
* @return string|false ISO month name, or false if the input was invalid
*/
private function makeIsoMonth( $monthName ) {
- $isoMonth = $this->xMonths[$this->lang->lc( $monthName )] ?? false;
+ $isoMonth = $this->xMonths[mb_strtolower( $monthName )] ?? false;
if ( $isoMonth === false ) {
return false;
}
* @return string ISO year name
*/
private function makeIsoYear( $year ) {
- # Assumes the year is in a nice format, as enforced by the regex
+ // Assumes the year is in a nice format, as enforced by the regex
if ( substr( $year, -2 ) == 'BC' ) {
$num = intval( substr( $year, 0, -3 ) ) - 1;
- # PHP bug note: sprintf( "%04d", -1 ) fails poorly
+ // PHP bug note: sprintf( "%04d", -1 ) fails poorly
$text = sprintf( '-%04d', $num );
-
} else {
$text = sprintf( '%04d', $year );
}
}
/**
- * Make a year one from an ISO year, for instance: '400 BC' from '-0399'.
+ * Make a year from an ISO year, for instance: '400 BC' from '-0399'.
* @param string $iso ISO year
* @return int|string int representing year number in case of AD dates, or string containing
* year number and 'BC' at the end otherwise.
--- /dev/null
+<?php
+
+class DateFormatterFactory {
+ /** @var DateFormatter[] */
+ private $instances;
+
+ /**
+ * @param Language $lang
+ * @return DateFormatter
+ */
+ public function get( Language $lang ) {
+ $code = $lang->getCode();
+ if ( !isset( $this->instances[$code] ) ) {
+ $this->instances[$code] = new DateFormatter( $lang );
+ }
+ return $this->instances[$code];
+ }
+}
use SpecialPage;
use Title;
use User;
+use Wikimedia\Assert\Assert;
/**
* Factory for handling the special page list and generating SpecialPage objects.
private $aliases;
/** @var Config */
- private $config;
+ private $options;
/** @var Language */
private $contLang;
/**
- * @param Config $config
+ * TODO Make this a const when HHVM support is dropped (T192166)
+ *
+ * @var array
+ * @since 1.33
+ * */
+ public static $constructorOptions = [
+ 'ContentHandlerUseDB',
+ 'DisableInternalSearch',
+ 'EmailAuthentication',
+ 'EnableEmail',
+ 'EnableJavaScriptTest',
+ 'PageLanguageUseDB',
+ 'SpecialPages',
+ ];
+
+ /**
+ * @param array $options
* @param Language $contLang
*/
- public function __construct( Config $config, Language $contLang ) {
- $this->config = $config;
+ public function __construct( array $options, Language $contLang ) {
+ Assert::parameter( count( $options ) === count( self::$constructorOptions ) &&
+ !array_diff( self::$constructorOptions, array_keys( $options ) ),
+ '$options', 'Wrong set of options present' );
+ $this->options = $options;
$this->contLang = $contLang;
}
if ( !is_array( $this->list ) ) {
$this->list = self::$coreList;
- if ( !$this->config->get( 'DisableInternalSearch' ) ) {
+ if ( !$this->options['DisableInternalSearch'] ) {
$this->list['Search'] = \SpecialSearch::class;
}
- if ( $this->config->get( 'EmailAuthentication' ) ) {
+ if ( $this->options['EmailAuthentication'] ) {
$this->list['Confirmemail'] = \EmailConfirmation::class;
$this->list['Invalidateemail'] = \EmailInvalidation::class;
}
- if ( $this->config->get( 'EnableEmail' ) ) {
+ if ( $this->options['EnableEmail'] ) {
$this->list['ChangeEmail'] = \SpecialChangeEmail::class;
}
- if ( $this->config->get( 'EnableJavaScriptTest' ) ) {
+ if ( $this->options['EnableJavaScriptTest'] ) {
$this->list['JavaScriptTest'] = \SpecialJavaScriptTest::class;
}
- if ( $this->config->get( 'PageLanguageUseDB' ) ) {
+ if ( $this->options['PageLanguageUseDB'] ) {
$this->list['PageLanguage'] = \SpecialPageLanguage::class;
}
- if ( $this->config->get( 'ContentHandlerUseDB' ) ) {
+ if ( $this->options['ContentHandlerUseDB'] ) {
$this->list['ChangeContentModel'] = \SpecialChangeContentModel::class;
}
// Add extension special pages
- $this->list = array_merge( $this->list, $this->config->get( 'SpecialPages' ) );
+ $this->list = array_merge( $this->list, $this->options['SpecialPages'] );
// This hook can be used to disable unwanted core special pages
// or conditionally register special pages.
* @return array
*/
protected function getFormFields() {
- global $wgBlockAllowsUTEdit;
+ $conf = $this->getConfig();
+ $enablePartialBlocks = $conf->get( 'EnablePartialBlocks' );
+ $blockAllowsUTEdit = $conf->get( 'BlockAllowsUTEdit' );
$this->getOutput()->enableOOUI();
$suggestedDurations = self::getSuggestedDurations();
- $conf = $this->getConfig();
- $enablePartialBlocks = $conf->get( 'EnablePartialBlocks' );
-
$a = [];
$a['Target'] = [
];
}
- if ( $wgBlockAllowsUTEdit ) {
+ if ( $blockAllowsUTEdit ) {
$a['DisableUTEdit'] = [
'type' => 'check',
'label-message' => 'ipb-disableusertalk',
'Victor Vasiliev', 'Rotem Liss', 'Platonides', 'Antoine Musso',
'Timo Tijhof', 'Daniel Kinzler', 'Jeroen De Dauw', 'Brad Jorsch',
'Bartosz Dziewoński', 'Ed Sanders', 'Moriel Schottlender',
+ 'Kunal Mehta', 'James D. Forrester', 'Brian Wolff', 'Adam Shorland',
$othersLink, $translatorsLink
];
}
// If the page is watched by the user (or may be watched), update the timestamp
- $job = new ClearWatchlistNotificationsJob(
- $user->getUserPage(),
- [ 'userId' => $user->getId(), 'timestamp' => $timestamp, 'casTime' => time() ]
- );
+ $job = new ClearWatchlistNotificationsJob( [
+ 'userId' => $user->getId(), 'timestamp' => $timestamp, 'casTime' => time()
+ ] );
// Try to run this post-send
// Calls DeferredUpdates::addCallableUpdate in normal operation
"page_first": "first",
"page_last": "last",
"histlegend": "Diff selection: Mark the radio boxes of the revisions to compare and hit enter or the button at the bottom.<br />\nLegend: <strong>({{int:cur}})</strong> = difference with latest revision, <strong>({{int:last}})</strong> = difference with preceding revision, <strong>{{int:minoreditletter}}</strong> = minor edit.",
- "history-fieldset-title": "Search for revisions",
+ "history-fieldset-title": "Filter revisions",
"history-show-deleted": "Revision deleted only",
"history_copyright": "-",
"histfirst": "oldest",
"page_first": "This is part of the navigation message on the top and bottom of Special pages which are lists of things in alphabetical order, e.g. the '[[Special:Categories|Categories]]' special page. It is followed by the message {{msg-mw|Viewprevnext}}.\n{{Identical|First}}",
"page_last": "This is part of the navigation message on the top and bottom of Special pages which are lists of things in alphabetical order, e.g. the '[[Special:Categories|Categories]]' special page. It is followed by the message {{msg-mw|Viewprevnext}}.\n\n{{Identical|Last}}",
"histlegend": "Text in history page.\n\nSee also:\n* {{msg-mw|Cur}}\n* {{msg-mw|Last}}\n* {{msg-mw|Minoreditletter}}",
- "history-fieldset-title": "Fieldset label in the edit history pages.",
+ "history-fieldset-title": "Form legend label in the edit history page.",
"history-show-deleted": "CheckBox to show only per [[mw:Manual:RevisionDelete|RevisionDelete]] deleted versions.\n\nUsed in History and [[Special:Contributions]].",
"history_copyright": "{{notranslate}}",
"histfirst": "This is part of the navigation message on the top and bottom of Page History pages which are lists of things in date order, e.g. [{{canonicalurl:Support|action=history}} Page History of Support].\n\nIt is followed by the message {{msg-mw|Viewprevnext}}.\n{{Identical|Oldest}}",
],
],
'mediawiki.special.block' => [
- 'scripts' => 'resources/src/mediawiki.special.block.js',
+ 'localBasePath' => "$IP/resources/src",
+ 'remoteBasePath' => "$wgResourceBasePath/resources/src",
+ 'packageFiles' => [
+ 'mediawiki.special.block.js',
+ [ 'name' => 'config.json', 'config' => [
+ 'EnablePartialBlocks',
+ 'BlockAllowsUTEdit',
+ ] ],
+ ],
'dependencies' => [
'oojs-ui-core',
'oojs-ui.styles.icons-editing-core',
type: multi-file
files:
mustache.js:
- src: https://raw.githubusercontent.com/janl/mustache.js/v1.0.0/mustache.js
- integrity: sha384-k2UYqmzoiq/qgIzZvcYBxbXQW4YdPAsXDOTkHTGb9TCZ9sjCkyT4TlaUN0wQRkql
+ src: https://raw.githubusercontent.com/janl/mustache.js/v3.0.1/mustache.js
+ integrity: sha384-YjAj6Nll7fkEWzxTlE9o3NWC9qdZt1Upat6Afjib9eLs8lTODpSKEBHeXq8o/VUH
LICENSE:
- src: https://raw.githubusercontent.com/janl/mustache.js/v1.0.0/LICENSE
- integrity: sha384-MYVwXwula9+YkyXexOJVZ0v0DaVvG22uX57mNq5Di+7u8OH9EG9q3yuXkp1Iehiq
+ src: https://raw.githubusercontent.com/janl/mustache.js/v3.0.1/LICENSE
+ integrity: sha384-j2EDj6YtCRgFvYDtzo6pXzbskIj/K1Yg65BL0j3/L6UIHxbMtRMJwC/W+XoYx0FZ
oojs:
type: tar
Copyright (c) 2009 Chris Wanstrath (Ruby)
Copyright (c) 2010-2014 Jan Lehnardt (JavaScript)
+Copyright (c) 2010-2015 The mustache.js community
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
* http://github.com/janl/mustache.js
*/
-/*global define: false*/
+/*global define: false Mustache: true*/
-(function (global, factory) {
- if (typeof exports === "object" && exports) {
+(function defineMustache (global, factory) {
+ if (typeof exports === 'object' && exports && typeof exports.nodeName !== 'string') {
factory(exports); // CommonJS
- } else if (typeof define === "function" && define.amd) {
+ } else if (typeof define === 'function' && define.amd) {
define(['exports'], factory); // AMD
} else {
- factory(global.Mustache = {}); // <script>
+ global.Mustache = {};
+ factory(global.Mustache); // script, wsh, asp
}
-}(this, function (mustache) {
+}(this, function mustacheFactory (mustache) {
- var Object_toString = Object.prototype.toString;
- var isArray = Array.isArray || function (object) {
- return Object_toString.call(object) === '[object Array]';
+ var objectToString = Object.prototype.toString;
+ var isArray = Array.isArray || function isArrayPolyfill (object) {
+ return objectToString.call(object) === '[object Array]';
};
- function isFunction(object) {
+ function isFunction (object) {
return typeof object === 'function';
}
- function escapeRegExp(string) {
- return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
+ /**
+ * More correct typeof string handling array
+ * which normally returns typeof 'object'
+ */
+ function typeStr (obj) {
+ return isArray(obj) ? 'array' : typeof obj;
+ }
+
+ function escapeRegExp (string) {
+ return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&');
+ }
+
+ /**
+ * Null safe way of checking whether or not an object,
+ * including its prototype, has a given property
+ */
+ function hasProperty (obj, propName) {
+ return obj != null && typeof obj === 'object' && (propName in obj);
+ }
+
+ /**
+ * Safe way of detecting whether or not the given thing is a primitive and
+ * whether it has the given property
+ */
+ function primitiveHasOwnProperty (primitive, propName) {
+ return (
+ primitive != null
+ && typeof primitive !== 'object'
+ && primitive.hasOwnProperty
+ && primitive.hasOwnProperty(propName)
+ );
}
// Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
// See https://github.com/janl/mustache.js/issues/189
- var RegExp_test = RegExp.prototype.test;
- function testRegExp(re, string) {
- return RegExp_test.call(re, string);
+ var regExpTest = RegExp.prototype.test;
+ function testRegExp (re, string) {
+ return regExpTest.call(re, string);
}
var nonSpaceRe = /\S/;
- function isWhitespace(string) {
+ function isWhitespace (string) {
return !testRegExp(nonSpaceRe, string);
}
var entityMap = {
- "&": "&",
- "<": "<",
- ">": ">",
+ '&': '&',
+ '<': '<',
+ '>': '>',
'"': '"',
"'": ''',
- "/": '/'
+ '/': '/',
+ '`': '`',
+ '=': '='
};
- function escapeHtml(string) {
- return String(string).replace(/[&<>"'\/]/g, function (s) {
+ function escapeHtml (string) {
+ return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap (s) {
return entityMap[s];
});
}
* array of tokens in the subtree and 2) the index in the original template at
* which the closing tag for that section begins.
*/
- function parseTemplate(template, tags) {
+ function parseTemplate (template, tags) {
if (!template)
return [];
// Strips all whitespace tokens array for the current line
// if there was a {{#tag}} on it and otherwise only space.
- function stripSpace() {
+ function stripSpace () {
if (hasTag && !nonSpace) {
while (spaces.length)
delete tokens[spaces.pop()];
}
var openingTagRe, closingTagRe, closingCurlyRe;
- function compileTags(tags) {
- if (typeof tags === 'string')
- tags = tags.split(spaceRe, 2);
+ function compileTags (tagsToCompile) {
+ if (typeof tagsToCompile === 'string')
+ tagsToCompile = tagsToCompile.split(spaceRe, 2);
- if (!isArray(tags) || tags.length !== 2)
- throw new Error('Invalid tags: ' + tags);
+ if (!isArray(tagsToCompile) || tagsToCompile.length !== 2)
+ throw new Error('Invalid tags: ' + tagsToCompile);
- openingTagRe = new RegExp(escapeRegExp(tags[0]) + '\\s*');
- closingTagRe = new RegExp('\\s*' + escapeRegExp(tags[1]));
- closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tags[1]));
+ openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + '\\s*');
+ closingTagRe = new RegExp('\\s*' + escapeRegExp(tagsToCompile[1]));
+ closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tagsToCompile[1]));
}
compileTags(tags || mustache.tags);
* Combines the values of consecutive text tokens in the given `tokens` array
* to a single token.
*/
- function squashTokens(tokens) {
+ function squashTokens (tokens) {
var squashedTokens = [];
var token, lastToken;
* all tokens that appear in that section and 2) the index in the original
* template that represents the end of that section.
*/
- function nestTokens(tokens) {
+ function nestTokens (tokens) {
var nestedTokens = [];
var collector = nestedTokens;
var sections = [];
token = tokens[i];
switch (token[0]) {
- case '#':
- case '^':
- collector.push(token);
- sections.push(token);
- collector = token[4] = [];
- break;
- case '/':
- section = sections.pop();
- section[5] = token[2];
- collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens;
- break;
- default:
- collector.push(token);
+ case '#':
+ case '^':
+ collector.push(token);
+ sections.push(token);
+ collector = token[4] = [];
+ break;
+ case '/':
+ section = sections.pop();
+ section[5] = token[2];
+ collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens;
+ break;
+ default:
+ collector.push(token);
}
}
* A simple string scanner that is used by the template parser to find
* tokens in template strings.
*/
- function Scanner(string) {
+ function Scanner (string) {
this.string = string;
this.tail = string;
this.pos = 0;
/**
* Returns `true` if the tail is empty (end of string).
*/
- Scanner.prototype.eos = function () {
- return this.tail === "";
+ Scanner.prototype.eos = function eos () {
+ return this.tail === '';
};
/**
* Tries to match the given regular expression at the current position.
* Returns the matched text if it can match, the empty string otherwise.
*/
- Scanner.prototype.scan = function (re) {
+ Scanner.prototype.scan = function scan (re) {
var match = this.tail.match(re);
if (!match || match.index !== 0)
* Skips all text until the given regular expression can be matched. Returns
* the skipped string, which is the entire tail if no match can be made.
*/
- Scanner.prototype.scanUntil = function (re) {
+ Scanner.prototype.scanUntil = function scanUntil (re) {
var index = this.tail.search(re), match;
switch (index) {
- case -1:
- match = this.tail;
- this.tail = "";
- break;
- case 0:
- match = "";
- break;
- default:
- match = this.tail.substring(0, index);
- this.tail = this.tail.substring(index);
+ case -1:
+ match = this.tail;
+ this.tail = '';
+ break;
+ case 0:
+ match = '';
+ break;
+ default:
+ match = this.tail.substring(0, index);
+ this.tail = this.tail.substring(index);
}
this.pos += match.length;
* Represents a rendering context by wrapping a view object and
* maintaining a reference to the parent context.
*/
- function Context(view, parentContext) {
- this.view = view == null ? {} : view;
+ function Context (view, parentContext) {
+ this.view = view;
this.cache = { '.': this.view };
this.parent = parentContext;
}
* Creates a new context using the given view with this context
* as the parent.
*/
- Context.prototype.push = function (view) {
+ Context.prototype.push = function push (view) {
return new Context(view, this);
};
* Returns the value of the given name in this context, traversing
* up the context hierarchy if the value is absent in this context's view.
*/
- Context.prototype.lookup = function (name) {
+ Context.prototype.lookup = function lookup (name) {
var cache = this.cache;
var value;
- if (name in cache) {
+ if (cache.hasOwnProperty(name)) {
value = cache[name];
} else {
- var context = this, names, index;
+ var context = this, intermediateValue, names, index, lookupHit = false;
while (context) {
if (name.indexOf('.') > 0) {
- value = context.view;
+ intermediateValue = context.view;
names = name.split('.');
index = 0;
- while (value != null && index < names.length)
- value = value[names[index++]];
- } else if (typeof context.view == 'object') {
- value = context.view[name];
+ /**
+ * Using the dot notion path in `name`, we descend through the
+ * nested objects.
+ *
+ * To be certain that the lookup has been successful, we have to
+ * check if the last object in the path actually has the property
+ * we are looking for. We store the result in `lookupHit`.
+ *
+ * This is specially necessary for when the value has been set to
+ * `undefined` and we want to avoid looking up parent contexts.
+ *
+ * In the case where dot notation is used, we consider the lookup
+ * to be successful even if the last "object" in the path is
+ * not actually an object but a primitive (e.g., a string, or an
+ * integer), because it is sometimes useful to access a property
+ * of an autoboxed primitive, such as the length of a string.
+ **/
+ while (intermediateValue != null && index < names.length) {
+ if (index === names.length - 1)
+ lookupHit = (
+ hasProperty(intermediateValue, names[index])
+ || primitiveHasOwnProperty(intermediateValue, names[index])
+ );
+
+ intermediateValue = intermediateValue[names[index++]];
+ }
+ } else {
+ intermediateValue = context.view[name];
+
+ /**
+ * Only checking against `hasProperty`, which always returns `false` if
+ * `context.view` is not an object. Deliberately omitting the check
+ * against `primitiveHasOwnProperty` if dot notation is not used.
+ *
+ * Consider this example:
+ * ```
+ * Mustache.render("The length of a football field is {{#length}}{{length}}{{/length}}.", {length: "100 yards"})
+ * ```
+ *
+ * If we were to check also against `primitiveHasOwnProperty`, as we do
+ * in the dot notation case, then render call would return:
+ *
+ * "The length of a football field is 9."
+ *
+ * rather than the expected:
+ *
+ * "The length of a football field is 100 yards."
+ **/
+ lookupHit = hasProperty(context.view, name);
}
- if (value != null)
+ if (lookupHit) {
+ value = intermediateValue;
break;
+ }
context = context.parent;
}
* string, given a context. It also maintains a cache of templates to
* avoid the need to parse the same template twice.
*/
- function Writer() {
+ function Writer () {
this.cache = {};
}
/**
* Clears all cached templates in this writer.
*/
- Writer.prototype.clearCache = function () {
+ Writer.prototype.clearCache = function clearCache () {
this.cache = {};
};
/**
- * Parses and caches the given `template` and returns the array of tokens
+ * Parses and caches the given `template` according to the given `tags` or
+ * `mustache.tags` if `tags` is omitted, and returns the array of tokens
* that is generated from the parse.
*/
- Writer.prototype.parse = function (template, tags) {
+ Writer.prototype.parse = function parse (template, tags) {
var cache = this.cache;
- var tokens = cache[template];
+ var cacheKey = template + ':' + (tags || mustache.tags).join(':');
+ var tokens = cache[cacheKey];
if (tokens == null)
- tokens = cache[template] = parseTemplate(template, tags);
+ tokens = cache[cacheKey] = parseTemplate(template, tags);
return tokens;
};
* names and templates of partials that are used in the template. It may
* also be a function that is used to load partial templates on the fly
* that takes a single argument: the name of the partial.
+ *
+ * If the optional `tags` argument is given here it must be an array with two
+ * string values: the opening and closing tags used in the template (e.g.
+ * [ "<%", "%>" ]). The default is to mustache.tags.
*/
- Writer.prototype.render = function (template, view, partials) {
- var tokens = this.parse(template);
+ Writer.prototype.render = function render (template, view, partials, tags) {
+ var tokens = this.parse(template, tags);
var context = (view instanceof Context) ? view : new Context(view);
- return this.renderTokens(tokens, context, partials, template);
+ return this.renderTokens(tokens, context, partials, template, tags);
};
/**
* If the template doesn't use higher-order sections, this argument may
* be omitted.
*/
- Writer.prototype.renderTokens = function (tokens, context, partials, originalTemplate) {
+ Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate, tags) {
var buffer = '';
- // This function is used to render an arbitrary template
- // in the current context by higher-order sections.
- var self = this;
- function subRender(template) {
- return self.render(template, context, partials);
- }
-
- var token, value;
+ var token, symbol, value;
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
+ value = undefined;
token = tokens[i];
+ symbol = token[0];
- switch (token[0]) {
- case '#':
- value = context.lookup(token[1]);
+ if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate);
+ else if (symbol === '^') value = this.renderInverted(token, context, partials, originalTemplate);
+ else if (symbol === '>') value = this.renderPartial(token, context, partials, tags);
+ else if (symbol === '&') value = this.unescapedValue(token, context);
+ else if (symbol === 'name') value = this.escapedValue(token, context);
+ else if (symbol === 'text') value = this.rawValue(token);
- if (!value)
- continue;
+ if (value !== undefined)
+ buffer += value;
+ }
- if (isArray(value)) {
- for (var j = 0, valueLength = value.length; j < valueLength; ++j) {
- buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate);
- }
- } else if (typeof value === 'object' || typeof value === 'string') {
- buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate);
- } else if (isFunction(value)) {
- if (typeof originalTemplate !== 'string')
- throw new Error('Cannot use higher-order sections without the original template');
+ return buffer;
+ };
- // Extract the portion of the original template that the section contains.
- value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);
+ Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate) {
+ var self = this;
+ var buffer = '';
+ var value = context.lookup(token[1]);
- if (value != null)
- buffer += value;
- } else {
- buffer += this.renderTokens(token[4], context, partials, originalTemplate);
- }
+ // This function is used to render an arbitrary template
+ // in the current context by higher-order sections.
+ function subRender (template) {
+ return self.render(template, context, partials);
+ }
- break;
- case '^':
- value = context.lookup(token[1]);
+ if (!value) return;
- // Use JavaScript's definition of falsy. Include empty arrays.
- // See https://github.com/janl/mustache.js/issues/186
- if (!value || (isArray(value) && value.length === 0))
- buffer += this.renderTokens(token[4], context, partials, originalTemplate);
+ if (isArray(value)) {
+ for (var j = 0, valueLength = value.length; j < valueLength; ++j) {
+ buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate);
+ }
+ } else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') {
+ buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate);
+ } else if (isFunction(value)) {
+ if (typeof originalTemplate !== 'string')
+ throw new Error('Cannot use higher-order sections without the original template');
- break;
- case '>':
- if (!partials)
- continue;
+ // Extract the portion of the original template that the section contains.
+ value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);
- value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
+ if (value != null)
+ buffer += value;
+ } else {
+ buffer += this.renderTokens(token[4], context, partials, originalTemplate);
+ }
+ return buffer;
+ };
- if (value != null)
- buffer += this.renderTokens(this.parse(value), context, partials, value);
+ Writer.prototype.renderInverted = function renderInverted (token, context, partials, originalTemplate) {
+ var value = context.lookup(token[1]);
- break;
- case '&':
- value = context.lookup(token[1]);
+ // Use JavaScript's definition of falsy. Include empty arrays.
+ // See https://github.com/janl/mustache.js/issues/186
+ if (!value || (isArray(value) && value.length === 0))
+ return this.renderTokens(token[4], context, partials, originalTemplate);
+ };
- if (value != null)
- buffer += value;
+ Writer.prototype.renderPartial = function renderPartial (token, context, partials, tags) {
+ if (!partials) return;
- break;
- case 'name':
- value = context.lookup(token[1]);
+ var value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
+ if (value != null)
+ return this.renderTokens(this.parse(value, tags), context, partials, value);
+ };
- if (value != null)
- buffer += mustache.escape(value);
+ Writer.prototype.unescapedValue = function unescapedValue (token, context) {
+ var value = context.lookup(token[1]);
+ if (value != null)
+ return value;
+ };
- break;
- case 'text':
- buffer += token[1];
- break;
- }
- }
+ Writer.prototype.escapedValue = function escapedValue (token, context) {
+ var value = context.lookup(token[1]);
+ if (value != null)
+ return mustache.escape(value);
+ };
- return buffer;
+ Writer.prototype.rawValue = function rawValue (token) {
+ return token[1];
};
- mustache.name = "mustache.js";
- mustache.version = "1.0.0";
- mustache.tags = [ "{{", "}}" ];
+ mustache.name = 'mustache.js';
+ mustache.version = '3.0.1';
+ mustache.tags = [ '{{', '}}' ];
// All high-level mustache.* functions use this writer.
var defaultWriter = new Writer();
/**
* Clears all cached templates in the default writer.
*/
- mustache.clearCache = function () {
+ mustache.clearCache = function clearCache () {
return defaultWriter.clearCache();
};
* array of tokens it contains. Doing this ahead of time avoids the need to
* parse templates on the fly as they are rendered.
*/
- mustache.parse = function (template, tags) {
+ mustache.parse = function parse (template, tags) {
return defaultWriter.parse(template, tags);
};
/**
* Renders the `template` with the given `view` and `partials` using the
- * default writer.
+ * default writer. If the optional `tags` argument is given here it must be an
+ * array with two string values: the opening and closing tags used in the
+ * template (e.g. [ "<%", "%>" ]). The default is to mustache.tags.
*/
- mustache.render = function (template, view, partials) {
- return defaultWriter.render(template, view, partials);
+ mustache.render = function render (template, view, partials, tags) {
+ if (typeof template !== 'string') {
+ throw new TypeError('Invalid template! Template should be a "string" ' +
+ 'but "' + typeStr(template) + '" was given as the first ' +
+ 'argument for mustache#render(template, view, partials)');
+ }
+
+ return defaultWriter.render(template, view, partials, tags);
};
- // This is here for backwards compatibility with 0.4.x.
- mustache.to_html = function (template, view, partials, send) {
+ // This is here for backwards compatibility with 0.4.x.,
+ /*eslint-disable */ // eslint wants camel cased function name
+ mustache.to_html = function to_html (template, view, partials, send) {
+ /*eslint-enable*/
+
var result = mustache.render(template, view, partials);
if (isFunction(send)) {
mustache.Context = Context;
mustache.Writer = Writer;
+ return mustache;
}));
}
$( function () {
- // This code is also loaded on the "block succeeded" page where there is no form,
- // so username and expiry fields might also be missing.
- var blockTargetWidget = infuseIfExists( $( '#mw-bi-target' ) ),
- anonOnlyField = infuseIfExists( $( '#mw-input-wpHardBlock' ).closest( '.oo-ui-fieldLayout' ) ),
- enableAutoblockField = infuseIfExists( $( '#mw-input-wpAutoBlock' ).closest( '.oo-ui-fieldLayout' ) ),
- hideUserWidget = infuseIfExists( $( '#mw-input-wpHideUser' ) ),
- hideUserField = infuseIfExists( $( '#mw-input-wpHideUser' ).closest( '.oo-ui-fieldLayout' ) ),
- watchUserField = infuseIfExists( $( '#mw-input-wpWatch' ).closest( '.oo-ui-fieldLayout' ) ),
- expiryWidget = infuseIfExists( $( '#mw-input-wpExpiry' ) ),
- editingWidget = infuseIfExists( $( '#mw-input-wpEditing' ) ),
- editingRestrictionWidget = infuseIfExists( $( '#mw-input-wpEditingRestriction' ) ),
- preventTalkPageEdit = infuseIfExists( $( '#mw-input-wpDisableUTEdit' ) ),
- pageRestrictionsWidget = infuseIfExists( $( '#mw-input-wpPageRestrictions' ) ),
- namespaceRestrictionsWidget = infuseIfExists( $( '#mw-input-wpNamespaceRestrictions' ) ),
- createAccountWidget = infuseIfExists( $( '#mw-input-wpCreateAccount' ) ),
- userChangedCreateAccount = mw.config.get( 'wgCreateAccountDirty' ),
- updatingBlockOptions = false;
+ var blockTargetWidget, anonOnlyWidget, enableAutoblockWidget, hideUserWidget, watchUserWidget,
+ expiryWidget, editingWidget, editingRestrictionWidget, preventTalkPageEditWidget,
+ pageRestrictionsWidget, namespaceRestrictionsWidget, createAccountWidget, data,
+ enablePartialBlocks, blockAllowsUTEdit, userChangedCreateAccount, updatingBlockOptions;
function updateBlockOptions() {
var blocktarget = blockTargetWidget.getValue().trim(),
// infinityValues are the values the SpecialBlock class accepts as infinity (sf. wfIsInfinity)
infinityValues = [ 'infinite', 'indefinite', 'infinity', 'never' ],
isIndefinite = infinityValues.indexOf( expiryValue ) !== -1,
- // editingRestrictionWidget only exists if partial blocks is enabled; if not, block must be sitewide
- editingRestrictionValue = editingRestrictionWidget ? editingRestrictionWidget.getValue() : 'sitewide',
- editingIsSelected = editingWidget ? editingWidget.isSelected() : false,
+ editingRestrictionValue = enablePartialBlocks ? editingRestrictionWidget.getValue() : 'sitewide',
+ editingIsSelected = editingWidget.isSelected(),
isSitewide = editingIsSelected && editingRestrictionValue === 'sitewide';
- if ( enableAutoblockField ) {
- enableAutoblockField.toggle( !isNonEmptyIp );
+ enableAutoblockWidget.setDisabled( isNonEmptyIp );
+ if ( enableAutoblockWidget.isDisabled() ) {
+ enableAutoblockWidget.setSelected( false );
+ }
+
+ anonOnlyWidget.setDisabled( !isIp && !isEmpty );
+ if ( anonOnlyWidget.isDisabled() ) {
+ anonOnlyWidget.setSelected( false );
}
- if ( hideUserField ) {
- hideUserField.toggle( !isNonEmptyIp && isIndefinite && isSitewide );
- if ( !hideUserField.isVisible() ) {
+
+ if ( hideUserWidget ) {
+ hideUserWidget.setDisabled( isNonEmptyIp || !isIndefinite || !isSitewide );
+ if ( hideUserWidget.isDisabled() ) {
hideUserWidget.setSelected( false );
}
}
- if ( anonOnlyField ) {
- anonOnlyField.toggle( isIp || isEmpty );
- }
- if ( watchUserField ) {
- watchUserField.toggle( !isIpRange || isEmpty );
+
+ if ( watchUserWidget ) {
+ watchUserWidget.setDisabled( isIpRange && !isEmpty );
+ if ( watchUserWidget.isDisabled() ) {
+ watchUserWidget.setSelected( false );
+ }
}
- if ( editingRestrictionWidget ) {
+
+ if ( enablePartialBlocks ) {
editingRestrictionWidget.setDisabled( !editingIsSelected );
- }
- if ( pageRestrictionsWidget ) {
pageRestrictionsWidget.setDisabled( !editingIsSelected || isSitewide );
- }
- if ( namespaceRestrictionsWidget ) {
namespaceRestrictionsWidget.setDisabled( !editingIsSelected || isSitewide );
+ if ( blockAllowsUTEdit ) {
+ // This option is disabled for partial blocks unless a namespace restriction
+ // for the User_talk namespace is in place.
+ preventTalkPageEditWidget.setDisabled(
+ editingIsSelected &&
+ editingRestrictionValue === 'partial' &&
+ namespaceRestrictionsWidget.getValue().indexOf(
+ String( mw.config.get( 'wgNamespaceIds' ).user_talk )
+ ) === -1
+ );
+ }
}
- if ( preventTalkPageEdit && namespaceRestrictionsWidget ) {
- // This option is disabled for partial blocks unless a namespace restriction
- // for the User_talk namespace is in place.
- preventTalkPageEdit.setDisabled(
- editingIsSelected &&
- editingRestrictionValue === 'partial' &&
- namespaceRestrictionsWidget.getValue().indexOf(
- String( mw.config.get( 'wgNamespaceIds' ).user_talk )
- ) === -1
- );
- }
+
if ( !userChangedCreateAccount ) {
updatingBlockOptions = true;
createAccountWidget.setSelected( isSitewide );
}
+ // This code is also loaded on the "block succeeded" page where there is no form,
+ // so check for block target widget; if it exists, the form is present
+ blockTargetWidget = infuseIfExists( $( '#mw-bi-target' ) );
+
if ( blockTargetWidget ) {
- // Bind functions so they're checked whenever stuff changes
+ data = require( './config.json' );
+ enablePartialBlocks = data.EnablePartialBlocks;
+ blockAllowsUTEdit = data.BlockAllowsUTEdit;
+ userChangedCreateAccount = mw.config.get( 'wgCreateAccountDirty' );
+ updatingBlockOptions = false;
+
+ // Always present if blockTargetWidget is present
+ editingWidget = OO.ui.infuse( $( '#mw-input-wpEditing' ) );
+ expiryWidget = OO.ui.infuse( $( '#mw-input-wpExpiry' ) );
+ createAccountWidget = OO.ui.infuse( $( '#mw-input-wpCreateAccount' ) );
+ enableAutoblockWidget = OO.ui.infuse( $( '#mw-input-wpAutoBlock' ) );
+ anonOnlyWidget = OO.ui.infuse( $( '#mw-input-wpHardBlock' ) );
blockTargetWidget.on( 'change', updateBlockOptions );
+ editingWidget.on( 'change', updateBlockOptions );
expiryWidget.on( 'change', updateBlockOptions );
- if ( editingWidget ) {
- editingWidget.on( 'change', updateBlockOptions );
- }
- if ( editingRestrictionWidget ) {
- editingRestrictionWidget.on( 'change', updateBlockOptions );
- }
- if ( namespaceRestrictionsWidget ) {
- namespaceRestrictionsWidget.on( 'change', updateBlockOptions );
- }
-
createAccountWidget.on( 'change', function () {
if ( !updatingBlockOptions ) {
userChangedCreateAccount = true;
}
} );
- // Call them now to set initial state (ie. Special:Block/Foobar?wpBlockExpiry=2+hours)
+ // Present for certain rights
+ watchUserWidget = infuseIfExists( $( '#mw-input-wpWatch' ) );
+ hideUserWidget = infuseIfExists( $( '#mw-input-wpHideUser' ) );
+
+ // Present for certain global configs
+ if ( enablePartialBlocks ) {
+ editingRestrictionWidget = OO.ui.infuse( $( '#mw-input-wpEditingRestriction' ) );
+ pageRestrictionsWidget = OO.ui.infuse( $( '#mw-input-wpPageRestrictions' ) );
+ namespaceRestrictionsWidget = OO.ui.infuse( $( '#mw-input-wpNamespaceRestrictions' ) );
+ editingRestrictionWidget.on( 'change', updateBlockOptions );
+ namespaceRestrictionsWidget.on( 'change', updateBlockOptions );
+ }
+ if ( blockAllowsUTEdit ) {
+ preventTalkPageEditWidget = infuseIfExists( $( '#mw-input-wpDisableUTEdit' ) );
+ }
+
updateBlockOptions();
}
} );
</p>
!! end
+!! test
+formatdate with invalid month
+!! wikitext
+{{#formatdate:2019-22-22|dmy}}
+!! html
+<p>2019-22-22
+</p>
+!! end
+
+!! test
+formatdate: dots in month name do not match any char (T220563)
+!! options
+language=de
+!! wikitext
+{{#formatdate:jun. 3|dmy}}
+{{#formatdate:junx 3|dmy}}
+!! html
+<p><span class="mw-formatted-date" title="06-03">3 Juni</span>
+junx 3
+</p>
+!! end
+
+!! test
+formatdate uses correct capitalisation in French
+!! options
+language=fr
+!! wikitext
+{{#formatdate:Juin 3|dmy}}
+!! html
+<p><span class="mw-formatted-date" title="06-03">3 juin</span>
+</p>
+!! end
+
+!! test
+formatdate uses correct capitalisation in English
+!! wikitext
+{{#formatdate:june 3|dmy}}
+!! html
+<p><span class="mw-formatted-date" title="06-03">3 June</span>
+</p>
+!! end
+
+
#
#
#
unlink( $readOnlyFile );
$this->setMwGlobals( [
+ 'wgReadOnly' => null,
'wgReadOnlyFile' => $readOnlyFile,
'wgUrlProtocols' => [
'http://',
* @covers ::wfReadOnly
*/
public function testReadOnlyEmpty() {
- global $wgReadOnly;
- $wgReadOnly = null;
-
- MediaWiki\MediaWikiServices::getInstance()->getReadOnlyMode()->clearCache();
$this->assertFalse( wfReadOnly() );
$this->assertFalse( wfReadOnly() );
}
* @covers ::wfReadOnly
*/
public function testReadOnlySet() {
- global $wgReadOnly, $wgReadOnlyFile;
-
- $readOnlyMode = MediaWiki\MediaWikiServices::getInstance()->getReadOnlyMode();
- $readOnlyMode->clearCache();
+ global $wgReadOnlyFile;
$f = fopen( $wgReadOnlyFile, "wt" );
fwrite( $f, 'Message' );
fclose( $f );
- $wgReadOnly = null; # Check on $wgReadOnlyFile
+
+ // Reset the service to avoid cached results
+ $this->overrideMwServices();
$this->assertTrue( wfReadOnly() );
$this->assertTrue( wfReadOnly() ); # Check cached
-
- unlink( $wgReadOnlyFile );
- $readOnlyMode->clearCache();
- $this->assertFalse( wfReadOnly() );
- $this->assertFalse( wfReadOnly() );
}
/**
*/
public function testReadOnlyGlobalChange() {
$this->assertFalse( wfReadOnlyReason() );
+
$this->setMwGlobals( [
'wgReadOnly' => 'reason'
] );
+ $this->overrideMwServices();
+
$this->assertSame( 'reason', wfReadOnlyReason() );
}
} ) );
$sortedNames = $names;
- sort( $sortedNames );
+ natcasesort( $sortedNames );
$this->assertSame( $sortedNames, $names,
'Please keep service getters sorted alphabetically' );
$rom->setReason( 'override' );
$this->assertSame( 'override', $rom->getReason() );
}
-
- /**
- * @covers ReadOnlyMode::clearCache
- * @covers ConfiguredReadOnlyMode::clearCache
- */
- public function testClearCache() {
- $fileName = $this->getNewTempFile();
- unlink( $fileName );
- $config = new HashConfig( [
- 'ReadOnly' => null,
- 'ReadOnlyFile' => $fileName,
- ] );
- $cro = new ConfiguredReadOnlyMode( $config );
- $lb = $this->createLB( [ 'lbMessage' => false ] );
- $rom = new ReadOnlyMode( $cro, $lb );
-
- $this->assertSame( false, $rom->getReason(), 'initial' );
-
- file_put_contents( $fileName, 'file' );
- $this->assertSame( false, $rom->getReason(), 'stale' );
-
- $rom->clearCache();
- $this->assertSame( 'file', $rom->getReason(), 'fresh' );
- }
}
global $IP;
$services = array_keys( require "$IP/includes/ServiceWiring.php" );
$sortedServices = $services;
- sort( $sortedServices );
+ natcasesort( $sortedServices );
$this->assertSame( $sortedServices, $services,
'Please keep services sorted alphabetically' );
$this->setService( 'MainWANObjectCache', $cache );
$jobq = JobQueueGroup::singleton();
- $jobq->push( new NullJob( Title::newMainPage(), [] ) );
+ $jobq->push( Job::factory( 'null', Title::newMainPage(), [] ) );
$this->assertEquals( 1, SiteStats::jobs(),
'A single job enqueued bumps jobscount stat to 1' );
- $jobq->push( new NullJob( Title::newMainPage(), [] ) );
+ $jobq->push( Job::factory( 'null', Title::newMainPage(), [] ) );
$this->assertEquals( 1, SiteStats::jobs(),
'SiteStats::jobs() count does not reflect addition ' .
'of a second job (cached)'
/**
* @dataProvider provideGetName
*/
- public function testGetName( $cacheBag, $insertCalls, $selectCalls ) {
+ public function testGetName( BagOStuff $cacheBag, $insertCalls, $selectCalls ) {
+ $now = microtime( true );
+ $cacheBag->setMockTime( $now );
// Check for operations to in-memory cache (IMC) and persistent cache (PC)
$store = $this->getNameTableSqlStore( $cacheBag, $insertCalls, $selectCalls );
// Get 1 ID and make sure getName returns correctly
$fooId = $store->acquireId( 'foo' ); // regen PC, set IMC, update IMC, tombstone PC
+ $now += 0.01;
$this->assertSame( 'foo', $store->getName( $fooId ) ); // use IMC
+ $now += 0.01;
// Get another ID and make sure getName returns correctly
$barId = $store->acquireId( 'bar' ); // update IMC, tombstone PC
+ $now += 0.01;
$this->assertSame( 'bar', $store->getName( $barId ) ); // use IMC
+ $now += 0.01;
// Blitz the cache and make sure it still returns
TestingAccessWrapper::newFromObject( $store )->tableCache = null; // clear IMC
// Blitz the cache again and get another ID and make sure getName returns correctly
TestingAccessWrapper::newFromObject( $store )->tableCache = null; // clear IMC
$bazId = $store->acquireId( 'baz' ); // set IMC using interim PC, update IMC, tombstone PC
+ $now += 0.01;
$this->assertSame( 'baz', $store->getName( $bazId ) ); // uses IMC
$this->assertSame( 'baz', $store->getName( $bazId ) ); // uses IMC
}
}
function newJob( $i = 0, $rootJob = [] ) {
- return new NullJob( Title::newMainPage(),
+ return Job::factory( 'null', Title::newMainPage(),
[ 'lives' => 0, 'usleep' => 0, 'removeDuplicates' => 0, 'i' => $i ] + $rootJob );
}
function newDedupedJob( $i = 0, $rootJob = [] ) {
- return new NullJob( Title::newMainPage(),
+ return Job::factory( 'null', Title::newMainPage(),
[ 'lives' => 0, 'usleep' => 0, 'removeDuplicates' => 1, 'i' => $i ] + $rootJob );
}
}
return [
[
$this->getMockJob( false ),
- 'someCommand ' . $requestId
+ 'someCommand Special: ' . $requestId
],
[
$this->getMockJob( [ 'key' => 'val' ] ),
- 'someCommand key=val ' . $requestId
+ 'someCommand Special: key=val ' . $requestId
],
[
$this->getMockJob( [ 'key' => [ 'inkey' => 'inval' ] ] ),
- 'someCommand key={"inkey":"inval"} ' . $requestId
+ 'someCommand Special: key={"inkey":"inval"} ' . $requestId
],
[
$this->getMockJob( [ 'val1' ] ),
- 'someCommand 0=val1 ' . $requestId
+ 'someCommand Special: 0=val1 ' . $requestId
],
[
$this->getMockJob( [ 'val1', 'val2' ] ),
- 'someCommand 0=val1 1=val2 ' . $requestId
+ 'someCommand Special: 0=val1 1=val2 ' . $requestId
],
[
$this->getMockJob( [ new stdClass() ] ),
- 'someCommand 0=object(stdClass) ' . $requestId
+ 'someCommand Special: 0=object(stdClass) ' . $requestId
],
[
$this->getMockJob( [ $mockToStringObj ] ),
- 'someCommand 0={STRING_OBJ_VAL} ' . $requestId
+ 'someCommand Special: 0={STRING_OBJ_VAL} ' . $requestId
],
[
$this->getMockJob( [
],
"triggeredRecursive" => true
] ),
- 'someCommand pages={"932737":[0,"Robert_James_Waller"]} ' .
+ 'someCommand Special: pages={"932737":[0,"Robert_James_Waller"]} ' .
'rootJobSignature=45868e99bba89064e4483743ebb9b682ef95c1a7 ' .
'rootJobTimestamp=20160309110158 masterPos=' .
'{"file":"db1023-bin.001288","pos":"308257743","asOfTime":' .
}
public function getMockJob( $params ) {
+ $title = new Title();
$mock = $this->getMockForAbstractClass(
Job::class,
- [ 'someCommand', new Title(), $params ],
+ [ 'someCommand', $title, $params ],
'SomeJob'
);
+
return $mock;
}
return [
'class name' => [ 'NullJob' ],
'closure' => [ function ( Title $title, array $params ) {
- return new NullJob( $title, $params );
+ return Job::factory( 'null', $title, $params );
} ],
'function' => [ [ $this, 'newNullJob' ] ],
'static function' => [ self::class . '::staticNullJob' ]
}
public function newNullJob( Title $title, array $params ) {
- return new NullJob( $title, $params );
+ return Job::factory( 'null', $title, $params );
}
public static function staticNullJob( Title $title, array $params ) {
- return new NullJob( $title, $params );
+ return Job::factory( 'null', $title, $params );
+ }
+
+ /**
+ * @covers Job::factory
+ * @covers Job::__construct()
+ */
+ public function testJobSignatureGeneric() {
+ $testPage = Title::makeTitle( NS_PROJECT, 'x' );
+ $blankTitle = Title::makeTitle( NS_SPECIAL, '' );
+ $params = [ 'z' => 1, 'lives' => 1, 'usleep' => 0 ];
+ $paramsWithTitle = $params + [ 'namespace' => NS_PROJECT, 'title' => 'x' ];
+
+ $job = new NullJob( [ 'namespace' => NS_PROJECT, 'title' => 'x' ] + $params );
+ $this->assertEquals( $testPage->getPrefixedText(), $job->getTitle()->getPrefixedText() );
+ $this->assertJobParamsMatch( $job, $paramsWithTitle );
+
+ $job = Job::factory( 'null', $testPage, $params );
+ $this->assertEquals( $blankTitle->getPrefixedText(), $job->getTitle()->getPrefixedText() );
+ $this->assertJobParamsMatch( $job, $params );
+
+ $job = Job::factory( 'null', $paramsWithTitle );
+ $this->assertEquals( $testPage->getPrefixedText(), $job->getTitle()->getPrefixedText() );
+ $this->assertJobParamsMatch( $job, $paramsWithTitle );
+
+ $job = Job::factory( 'null', $params );
+ $this->assertTrue( $blankTitle->equals( $job->getTitle() ) );
+ $this->assertJobParamsMatch( $job, $params );
+ }
+
+ /**
+ * @covers Job::factory
+ * @covers Job::__construct()
+ */
+ public function testJobSignatureTitleBased() {
+ $testPage = Title::makeTitle( NS_PROJECT, 'x' );
+ $blankTitle = Title::makeTitle( NS_SPECIAL, '' );
+ $params = [ 'z' => 1, 'causeAction' => 'unknown', 'causeAgent' => 'unknown' ];
+ $paramsWithTitle = $params + [ 'namespace' => NS_PROJECT, 'title' => 'x' ];
+
+ $job = new RefreshLinksJob( $testPage, $params );
+ $this->assertEquals( $testPage->getPrefixedText(), $job->getTitle()->getPrefixedText() );
+ $this->assertSame( $testPage, $job->getTitle() );
+ $this->assertJobParamsMatch( $job, $paramsWithTitle );
+ $this->assertSame( $testPage, $job->getTitle() );
+
+ $job = Job::factory( 'refreshLinks', $testPage, $params );
+ $this->assertEquals( $testPage->getPrefixedText(), $job->getTitle()->getPrefixedText() );
+ $this->assertJobParamsMatch( $job, $paramsWithTitle );
+
+ $job = Job::factory( 'refreshLinks', $paramsWithTitle );
+ $this->assertEquals( $testPage->getPrefixedText(), $job->getTitle()->getPrefixedText() );
+ $this->assertJobParamsMatch( $job, $paramsWithTitle );
+
+ $job = Job::factory( 'refreshLinks', $params );
+ $this->assertTrue( $blankTitle->equals( $job->getTitle() ) );
+ $this->assertJobParamsMatch( $job, $params );
+ }
+
+ /**
+ * @covers Job::factory
+ * @covers Job::__construct()
+ */
+ public function testJobSignatureTitleBasedIncomplete() {
+ $testPage = Title::makeTitle( NS_PROJECT, 'x' );
+ $blankTitle = Title::makeTitle( NS_SPECIAL, '' );
+ $params = [ 'z' => 1, 'causeAction' => 'unknown', 'causeAgent' => 'unknown' ];
+
+ $job = new RefreshLinksJob( $testPage, $params + [ 'namespace' => 0 ] );
+ $this->assertEquals( $blankTitle->getPrefixedText(), $job->getTitle()->getPrefixedText() );
+ $this->assertJobParamsMatch( $job, $params + [ 'namespace' => 0 ] );
+
+ $job = new RefreshLinksJob( $testPage, $params + [ 'title' => 'x' ] );
+ $this->assertEquals( $blankTitle->getPrefixedText(), $job->getTitle()->getPrefixedText() );
+ $this->assertJobParamsMatch( $job, $params + [ 'title' => 'x' ] );
}
+ private function assertJobParamsMatch( IJobSpecification $job, array $params ) {
+ $actual = $job->getParams();
+ unset( $actual['requestId'] );
+
+ $this->assertEquals( $actual, $params );
+ }
}
$this->setMwGlobals( 'wgUpdateRowsPerQuery', 2 );
JobQueueGroup::singleton()->push(
- new ClearUserWatchlistJob(
- null,
- [
- 'userId' => $user->getId(),
- 'maxWatchlistId' => $maxId,
- ]
- )
+ new ClearUserWatchlistJob( [
+ 'userId' => $user->getId(), 'maxWatchlistId' => $maxId,
+ ] )
);
$this->assertEquals( 1, JobQueueGroup::singleton()->getQueueSizes()['clearUserWatchlist'] );