Merge "Remove Preferences class"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 11 Apr 2019 16:19:15 +0000 (16:19 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 11 Apr 2019 16:19:15 +0000 (16:19 +0000)
18 files changed:
autoload.php
includes/MediaWikiServices.php
includes/Permissions/PermissionManager.php
includes/ServiceWiring.php
includes/db/DatabaseOracle.php
includes/db/MWLBFactory.php
includes/parser/CoreParserFunctions.php
includes/parser/DateFormatter.php
includes/parser/DateFormatterFactory.php [new file with mode: 0644]
includes/parser/Parser.php
includes/parser/ParserFactory.php
includes/specials/SpecialBlock.php
maintenance/language/generateCollationData.php
maintenance/language/generateNormalizerDataAr.php
maintenance/language/generateNormalizerDataMl.php
resources/Resources.php
resources/src/mediawiki.special.block.js
tests/parser/parserTests.txt

index c1c8e3e..a5ce46b 100644 (file)
@@ -364,6 +364,7 @@ $wgAutoloadLocalClasses = [
        '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',
index 8c60dc7..22f0c63 100644 (file)
@@ -7,6 +7,7 @@ use Config;
 use ConfigFactory;
 use CryptHKDF;
 use CryptRand;
+use DateFormatterFactory;
 use EventRelayerGroup;
 use GenderCache;
 use GlobalVarConfig;
@@ -526,6 +527,14 @@ class MediaWikiServices extends ServiceContainer {
                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.
index 1d94e0e..549b7ba 100644 (file)
@@ -27,7 +27,7 @@ use MediaWiki\Linker\LinkTarget;
 use MediaWiki\Special\SpecialPageFactory;
 use MessageSpecifier;
 use MWException;
-use MWNamespace;
+use NamespaceInfo;
 use RequestContext;
 use SpecialPage;
 use Title;
@@ -78,13 +78,15 @@ class PermissionManager {
                $whitelistRead,
                $whitelistReadRegexp,
                $emailConfirmToEdit,
-               $blockDisablesLogin
+               $blockDisablesLogin,
+               NamespaceInfo $nsInfo
        ) {
                $this->specialPageFactory = $specialPageFactory;
                $this->whitelistRead = $whitelistRead;
                $this->whitelistReadRegexp = $whitelistReadRegexp;
                $this->emailConfirmToEdit = $emailConfirmToEdit;
                $this->blockDisablesLogin = $blockDisablesLogin;
+               $this->nsInfo = $nsInfo;
        }
 
        /**
@@ -597,13 +599,15 @@ class PermissionManager {
                        return $errors;
                }
 
-               $isSubPage = MWNamespace::hasSubpages( $page->getNamespace() ) ?
+               $isSubPage = $this->nsInfo->hasSubpages( $page->getNamespace() ) ?
                        strpos( $page->getText(), '/' ) !== false : false;
 
                if ( $action == 'create' ) {
                        if (
-                               ( MWNamespace::isTalk( $page->getNamespace() ) && !$user->isAllowed( 'createtalk' ) ) ||
-                               ( !MWNamespace::isTalk( $page->getNamespace() ) && !$user->isAllowed( 'createpage' ) )
+                               ( $this->nsInfo->isTalk( $page->getNamespace() ) &&
+                                       !$user->isAllowed( 'createtalk' ) ) ||
+                               ( !$this->nsInfo->isTalk( $page->getNamespace() ) &&
+                                       !$user->isAllowed( 'createpage' ) )
                        ) {
                                $errors[] = $user->isAnon() ? [ 'nocreatetext' ] : [ 'nocreate-loggedin' ];
                        }
@@ -817,7 +821,7 @@ class PermissionManager {
                        }
                } elseif ( $action == 'move' ) {
                        // Check for immobile pages
-                       if ( !MWNamespace::isMovable( $page->getNamespace() ) ) {
+                       if ( !$this->nsInfo->isMovable( $page->getNamespace() ) ) {
                                // Specific message for this case
                                $errors[] = [ 'immobile-source-namespace', $page->getNsText() ];
                        } elseif ( !$page->isMovable() ) {
@@ -825,7 +829,7 @@ class PermissionManager {
                                $errors[] = [ 'immobile-source-page' ];
                        }
                } elseif ( $action == 'move-target' ) {
-                       if ( !MWNamespace::isMovable( $page->getNamespace() ) ) {
+                       if ( !$this->nsInfo->isMovable( $page->getNamespace() ) ) {
                                $errors[] = [ 'immobile-target-namespace', $page->getNsText() ];
                        } elseif ( !$page->isMovable() ) {
                                $errors[] = [ 'immobile-target-page' ];
index a82feaa..1eb3551 100644 (file)
@@ -138,6 +138,10 @@ return [
                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();
@@ -373,7 +377,8 @@ return [
                        wfUrlProtocols(),
                        $services->getSpecialPageFactory(),
                        $services->getMainConfig(),
-                       $services->getLinkRendererFactory()
+                       $services->getLinkRendererFactory(),
+                       $services->getNamespaceInfo()
                );
        },
 
@@ -402,7 +407,9 @@ return [
                        $config->get( 'WhitelistRead' ),
                        $config->get( 'WhitelistReadRegexp' ),
                        $config->get( 'EmailConfirmToEdit' ),
-                       $config->get( 'BlockDisablesLogin' ) );
+                       $config->get( 'BlockDisablesLogin' ),
+                       $services->getNamespaceInfo()
+               );
        },
 
        'PreferencesFactory' => function ( MediaWikiServices $services ) : PreferencesFactory {
index 3d80bbd..7f79ca1 100644 (file)
@@ -21,7 +21,7 @@
  * @ingroup Database
  */
 
-use MediaWiki\MediaWikiServices;
+use Wikimedia\Timestamp\ConvertibleTimestamp;
 use Wikimedia\Rdbms\Database;
 use Wikimedia\Rdbms\DatabaseDomain;
 use Wikimedia\Rdbms\Blob;
@@ -55,6 +55,8 @@ class DatabaseOracle extends Database {
        function __construct( array $p ) {
                $p['tablePrefix'] = strtoupper( $p['tablePrefix'] );
                parent::__construct( $p );
+
+               // @TODO: dependency inject
                Hooks::run( 'DatabaseOraclePostInit', [ $this ] );
        }
 
@@ -79,8 +81,6 @@ class DatabaseOracle extends Database {
        }
 
        protected function open( $server, $user, $password, $dbName, $schema, $tablePrefix ) {
-               global $wgDBOracleDRCP;
-
                if ( !function_exists( 'oci_connect' ) ) {
                        throw new DBConnectionError(
                                $this,
@@ -107,10 +107,6 @@ class DatabaseOracle extends Database {
                        return null;
                }
 
-               if ( $wgDBOracleDRCP ) {
-                       $this->setFlag( DBO_PERSISTENT );
-               }
-
                $session_mode = ( $this->flags & DBO_SYSDBA ) ? OCI_SYSDBA : OCI_DEFAULT;
 
                Wikimedia\suppressWarnings();
@@ -185,8 +181,8 @@ class DatabaseOracle extends Database {
         */
        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
@@ -420,7 +416,11 @@ class DatabaseOracle extends Database {
                }
 
                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';
@@ -481,12 +481,14 @@ class DatabaseOracle extends Database {
                                }
 
                                // 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__ );
@@ -498,7 +500,10 @@ class DatabaseOracle extends Database {
                                $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 ) ) {
@@ -554,7 +559,8 @@ class DatabaseOracle extends Database {
                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
@@ -573,7 +579,8 @@ class DatabaseOracle extends Database {
                        $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;
@@ -756,8 +763,10 @@ class DatabaseOracle extends Database {
                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 );
        }
 
        /**
@@ -912,7 +921,10 @@ class DatabaseOracle extends Database {
         */
        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 );
@@ -1061,12 +1073,7 @@ class DatabaseOracle extends Database {
        }
 
        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 ) {
@@ -1090,11 +1097,9 @@ class DatabaseOracle extends Database {
                $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 );
                }
        }
 
@@ -1260,12 +1265,14 @@ class DatabaseOracle extends Database {
                                        $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__ );
@@ -1277,7 +1284,10 @@ class DatabaseOracle extends Database {
                                $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 ) ) {
@@ -1366,4 +1376,16 @@ class DatabaseOracle extends Database {
        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." );
+       }
 }
index bbb3d2f..f0aa8b2 100644 (file)
@@ -53,7 +53,7 @@ abstract class MWLBFactory {
        ) {
                global $wgCommandLineMode;
 
-               static $typesWithSchema = [ 'postgres', 'msssql' ];
+               $typesWithSchema = self::getDbTypesWithSchemas();
 
                $lbConf += [
                        'localDomain' => new DatabaseDomain(
@@ -82,77 +82,29 @@ abstract class MWLBFactory {
                // 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'] ) ) {
@@ -167,15 +119,71 @@ abstract class MWLBFactory {
                                }
                                $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
@@ -183,7 +191,7 @@ abstract class MWLBFactory {
         * @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)
@@ -204,7 +212,7 @@ abstract class MWLBFactory {
         * @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
 
index d1d1a9c..b2b7486 100644 (file)
@@ -113,7 +113,7 @@ class CoreParserFunctions {
         */
        public static function formatDate( $parser, $date, $defaultPref = null ) {
                $lang = $parser->getFunctionLang();
-               $df = DateFormatter::getInstance( $lang );
+               $df = MediaWikiServices::getInstance()->getDateFormatterFactory()->get( $lang );
 
                $date = trim( $date );
 
index c9bbc43..b0c41d9 100644 (file)
 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;
        }
 
        /**
@@ -348,7 +292,7 @@ class DateFormatter {
         * @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;
                }
@@ -361,12 +305,11 @@ class DateFormatter {
         * @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 );
                }
@@ -374,7 +317,7 @@ class DateFormatter {
        }
 
        /**
-        * 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.
diff --git a/includes/parser/DateFormatterFactory.php b/includes/parser/DateFormatterFactory.php
new file mode 100644 (file)
index 0000000..d18ecf4
--- /dev/null
@@ -0,0 +1,18 @@
+<?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];
+       }
+}
index 47e5b40..9ff731d 100644 (file)
@@ -280,6 +280,9 @@ class Parser {
        /** @var LinkRendererFactory */
        private $linkRendererFactory;
 
+       /** @var NamespaceInfo */
+       private $nsInfo;
+
        /**
         * @param array $parserConf See $wgParserConf documentation
         * @param MagicWordFactory|null $magicWordFactory
@@ -289,12 +292,14 @@ class Parser {
         * @param SpecialPageFactory|null $spFactory
         * @param Config|null $siteConfig
         * @param LinkRendererFactory|null $linkRendererFactory
+        * @param NamespaceInfo|null $nsInfo
         */
        public function __construct(
                array $parserConf = [], MagicWordFactory $magicWordFactory = null,
                Language $contLang = null, ParserFactory $factory = null, $urlProtocols = null,
                SpecialPageFactory $spFactory = null, Config $siteConfig = null,
-               LinkRendererFactory $linkRendererFactory = null
+               LinkRendererFactory $linkRendererFactory = null,
+               NamespaceInfo $nsInfo = null
        ) {
                $this->mConf = $parserConf;
                $this->mUrlProtocols = $urlProtocols ?? wfUrlProtocols();
@@ -325,10 +330,10 @@ class Parser {
 
                $this->factory = $factory ?? $services->getParserFactory();
                $this->specialPageFactory = $spFactory ?? $services->getSpecialPageFactory();
-               $this->siteConfig = $siteConfig ?? MediaWikiServices::getInstance()->getMainConfig();
-
+               $this->siteConfig = $siteConfig ?? $services->getMainConfig();
                $this->linkRendererFactory =
-                       $linkRendererFactory ?? MediaWikiServices::getInstance()->getLinkRendererFactory();
+                       $linkRendererFactory ?? $services->getLinkRendererFactory();
+               $this->nsInfo = $nsInfo ?? $services->getNamespaceInfo();
        }
 
        /**
@@ -2529,7 +2534,7 @@ class Parser {
         */
        public function areSubpagesAllowed() {
                # Some namespaces don't allow subpages
-               return MWNamespace::hasSubpages( $this->mTitle->getNamespace() );
+               return $this->nsInfo->hasSubpages( $this->mTitle->getNamespace() );
        }
 
        /**
@@ -2600,7 +2605,7 @@ class Parser {
                        $this->siteConfig->get( 'MiserMode' ) &&
                        !$this->mOptions->getInterfaceMessage() &&
                        // @TODO: disallow this word on all namespaces
-                       MWNamespace::isContent( $this->mTitle->getNamespace() )
+                       $this->nsInfo->isContent( $this->mTitle->getNamespace() )
                ) {
                        return $this->mRevisionId ? '-' : '';
                };
@@ -3339,7 +3344,7 @@ class Parser {
                                                        );
                                                }
                                        }
-                               } elseif ( MWNamespace::isNonincludable( $title->getNamespace() ) ) {
+                               } elseif ( $this->nsInfo->isNonincludable( $title->getNamespace() ) ) {
                                        $found = false; # access denied
                                        wfDebug( __METHOD__ . ": template inclusion denied for " .
                                                $title->getPrefixedDBkey() . "\n" );
index 05c0622..cddacf4 100644 (file)
@@ -19,7 +19,7 @@
  * @ingroup Parser
  */
 use MediaWiki\Linker\LinkRendererFactory;
-
+use MediaWiki\MediaWikiServices;
 use MediaWiki\Special\SpecialPageFactory;
 
 /**
@@ -47,6 +47,9 @@ class ParserFactory {
        /** @var LinkRendererFactory */
        private $linkRendererFactory;
 
+       /** @var NamespaceInfo */
+       private $nsInfo;
+
        /**
         * @param array $parserConf See $wgParserConf documentation
         * @param MagicWordFactory $magicWordFactory
@@ -55,12 +58,18 @@ class ParserFactory {
         * @param SpecialPageFactory $spFactory
         * @param Config $siteConfig
         * @param LinkRendererFactory $linkRendererFactory
+        * @param NamespaceInfo|null $nsInfo
         * @since 1.32
         */
        public function __construct(
                array $parserConf, MagicWordFactory $magicWordFactory, Language $contLang, $urlProtocols,
-               SpecialPageFactory $spFactory, Config $siteConfig, LinkRendererFactory $linkRendererFactory
+               SpecialPageFactory $spFactory, Config $siteConfig,
+               LinkRendererFactory $linkRendererFactory, NamespaceInfo $nsInfo = null
        ) {
+               if ( !$nsInfo ) {
+                       wfDeprecated( __METHOD__ . ' with no NamespaceInfo argument', '1.34' );
+                       $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
+               }
                $this->parserConf = $parserConf;
                $this->magicWordFactory = $magicWordFactory;
                $this->contLang = $contLang;
@@ -68,6 +77,7 @@ class ParserFactory {
                $this->specialPageFactory = $spFactory;
                $this->siteConfig = $siteConfig;
                $this->linkRendererFactory = $linkRendererFactory;
+               $this->nsInfo = $nsInfo;
        }
 
        /**
@@ -77,6 +87,6 @@ class ParserFactory {
        public function create() : Parser {
                return new Parser( $this->parserConf, $this->magicWordFactory, $this->contLang, $this,
                        $this->urlProtocols, $this->specialPageFactory, $this->siteConfig,
-                       $this->linkRendererFactory );
+                       $this->linkRendererFactory, $this->nsInfo );
        }
 }
index 155d6a4..9e218b6 100644 (file)
@@ -141,7 +141,9 @@ class SpecialBlock extends FormSpecialPage {
         * @return array
         */
        protected function getFormFields() {
-               global $wgBlockAllowsUTEdit;
+               $conf = $this->getConfig();
+               $enablePartialBlocks = $conf->get( 'EnablePartialBlocks' );
+               $blockAllowsUTEdit = $conf->get( 'BlockAllowsUTEdit' );
 
                $this->getOutput()->enableOOUI();
 
@@ -149,9 +151,6 @@ class SpecialBlock extends FormSpecialPage {
 
                $suggestedDurations = self::getSuggestedDurations();
 
-               $conf = $this->getConfig();
-               $enablePartialBlocks = $conf->get( 'EnablePartialBlocks' );
-
                $a = [];
 
                $a['Target'] = [
@@ -232,7 +231,7 @@ class SpecialBlock extends FormSpecialPage {
                        ];
                }
 
-               if ( $wgBlockAllowsUTEdit ) {
+               if ( $blockAllowsUTEdit ) {
                        $a['DisableUTEdit'] = [
                                'type' => 'check',
                                'label-message' => 'ipb-disableusertalk',
index f43d75f..5843f67 100644 (file)
@@ -23,6 +23,8 @@
 
 require_once __DIR__ . '/../Maintenance.php';
 
+use Wikimedia\StaticArrayWriter;
+
 /**
  * Generate first letter data files for Collation.php
  *
index 336495a..e689f7c 100644 (file)
@@ -23,6 +23,8 @@
 
 require_once __DIR__ . '/../Maintenance.php';
 
+use Wikimedia\StaticArrayWriter;
+
 /**
  * Generates the normalizer data file for Arabic.
  *
index 1b8ea09..5f865ce 100644 (file)
@@ -23,6 +23,8 @@
 
 require_once __DIR__ . '/../Maintenance.php';
 
+use Wikimedia\StaticArrayWriter;
+
 /**
  * Generates the normalizer data file for Malayalam.
  *
index ba61488..174c7d9 100644 (file)
@@ -2100,7 +2100,15 @@ return [
                ],
        ],
        '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',
index b46df85..58657db 100644 (file)
        }
 
        $( 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();
                }
        } );
index ee33f1d..0facec2 100644 (file)
@@ -24146,6 +24146,49 @@ language=nl title=[[MediaWiki:Common.css]]
 </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
+
+
 #
 #
 #