Merge "Break PreferencesFormOOUI->PermissionManager dependency"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 17 Sep 2019 20:32:10 +0000 (20:32 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 17 Sep 2019 20:32:10 +0000 (20:32 +0000)
18 files changed:
includes/DefaultSettings.php
includes/Rest/EntryPoint.php
includes/Rest/LocalizedHttpException.php
includes/Rest/ResponseFactory.php
includes/Rest/Router.php
includes/password/PasswordPolicyChecks.php
languages/i18n/en.json
languages/i18n/qqq.json
maintenance/mediawiki.Title/generatePhpCharToUpperMappings.php
resources/src/mediawiki.Title/Title.js
resources/src/mediawiki.Title/phpCharToUpper.json
tests/phpunit/includes/Rest/BasicAccess/MWBasicRequestAuthorizerTest.php
tests/phpunit/includes/Rest/EntryPointTest.php
tests/phpunit/unit/includes/Rest/Handler/HelloHandlerTest.php
tests/phpunit/unit/includes/Rest/ResponseFactoryTest.php
tests/phpunit/unit/includes/Rest/RouterTest.php
tests/selenium/wdio-mediawiki/Api.js
tests/selenium/wdio-mediawiki/README.md

index c3a37f3..fd1affc 100644 (file)
@@ -4445,7 +4445,8 @@ $wgCentralIdLookupProvider = 'local';
  * The checks supported by core are:
  *     - MinimalPasswordLength - Minimum length a user can set.
  *     - MinimumPasswordLengthToLogin - Passwords shorter than this will
- *             not be allowed to login, regardless if it is correct.
+ *             not be allowed to login, or offered a chance to reset their password
+ *             as part of the login workflow, regardless if it is correct.
  *     - MaximalPasswordLength - maximum length password a user is allowed
  *             to attempt. Prevents DoS attacks with pbkdf2.
  *     - PasswordCannotMatchUsername - Password cannot match the username.
index ee3441e..4fdd1f8 100644 (file)
@@ -10,6 +10,7 @@ use MediaWiki\Rest\Validator\Validator;
 use RequestContext;
 use Title;
 use WebResponse;
+use Wikimedia\Message\ITextFormatter;
 
 class EntryPoint {
        /** @var RequestInterface */
@@ -49,6 +50,8 @@ class EntryPoint {
                        'cookiePrefix' => $conf->get( 'CookiePrefix' )
                ] );
 
+               $responseFactory = new ResponseFactory( self::getTextFormatters( $services ) );
+
                // @phan-suppress-next-line PhanAccessMethodInternal
                $authorizer = new MWBasicAuthorizer( $context->getUser(),
                        $services->getPermissionManager() );
@@ -62,7 +65,7 @@ class EntryPoint {
                        ExtensionRegistry::getInstance()->getAttribute( 'RestRoutes' ),
                        $conf->get( 'RestPath' ),
                        $services->getLocalServerObjectCache(),
-                       new ResponseFactory,
+                       $responseFactory,
                        $authorizer,
                        $objectFactory,
                        $restValidator
@@ -76,6 +79,25 @@ class EntryPoint {
                $entryPoint->execute();
        }
 
+       /**
+        * Get a TextFormatter array from MediaWikiServices
+        *
+        * @param MediaWikiServices $services
+        * @return ITextFormatter[]
+        */
+       public static function getTextFormatters( MediaWikiServices $services ) {
+               $langs = array_unique( [
+                       $services->getMainConfig()->get( 'ContLang' )->getCode(),
+                       'en'
+               ] );
+               $textFormatters = [];
+               $factory = $services->getMessageFormatterFactory();
+               foreach ( $langs as $lang ) {
+                       $textFormatters[] = $factory->getTextFormatter( $lang );
+               }
+               return $textFormatters;
+       }
+
        public function __construct( RequestContext $context, RequestInterface $request,
                WebResponse $webResponse, Router $router
        ) {
index 10d3a40..184fe16 100644 (file)
@@ -5,7 +5,14 @@ namespace MediaWiki\Rest;
 use Wikimedia\Message\MessageValue;
 
 class LocalizedHttpException extends HttpException {
-       public function __construct( MessageValue $message, $code = 500 ) {
-               parent::__construct( 'Localized exception with key ' . $message->getKey(), $code );
+       private $messageValue;
+
+       public function __construct( MessageValue $messageValue, $code = 500 ) {
+               parent::__construct( 'Localized exception with key ' . $messageValue->getKey(), $code );
+               $this->messageValue = $messageValue;
+       }
+
+       public function getMessageValue() {
+               return $this->messageValue;
        }
 }
index 5e5a198..fd0f3c7 100644 (file)
@@ -5,19 +5,31 @@ namespace MediaWiki\Rest;
 use Exception;
 use HttpStatus;
 use InvalidArgumentException;
+use LanguageCode;
 use MWExceptionHandler;
 use stdClass;
 use Throwable;
+use Wikimedia\Message\ITextFormatter;
+use Wikimedia\Message\MessageValue;
 
 /**
  * Generates standardized response objects.
  */
 class ResponseFactory {
-
        const CT_PLAIN = 'text/plain; charset=utf-8';
        const CT_HTML = 'text/html; charset=utf-8';
        const CT_JSON = 'application/json';
 
+       /** @var ITextFormatter[] */
+       private $textFormatters;
+
+       /**
+        * @param ITextFormatter[] $textFormatters
+        */
+       public function __construct( $textFormatters ) {
+               $this->textFormatters = $textFormatters;
+       }
+
        /**
         * Encode a stdClass object or array to a JSON string
         *
@@ -167,13 +179,23 @@ class ResponseFactory {
                return $response;
        }
 
+       /**
+        * Create an HTTP 4xx or 5xx response with error message localisation
+        */
+       public function createLocalizedHttpError( $errorCode, MessageValue $messageValue ) {
+               return $this->createHttpError( $errorCode, $this->formatMessage( $messageValue ) );
+       }
+
        /**
         * Turn an exception into a JSON error response.
         * @param Exception|Throwable $exception
         * @return Response
         */
        public function createFromException( $exception ) {
-               if ( $exception instanceof HttpException ) {
+               if ( $exception instanceof LocalizedHttpException ) {
+                       $response = $this->createLocalizedHttpError( $exception->getCode(),
+                               $exception->getMessageValue() );
+               } elseif ( $exception instanceof HttpException ) {
                        // FIXME can HttpException represent 2xx or 3xx responses?
                        $response = $this->createHttpError(
                                $exception->getCode(),
@@ -240,4 +262,18 @@ class ResponseFactory {
                return "<!doctype html><title>Redirect</title><a href=\"$url\">$url</a>";
        }
 
+       public function formatMessage( MessageValue $messageValue ) {
+               if ( !$this->textFormatters ) {
+                       // For unit tests
+                       return [];
+               }
+               $translations = [];
+               foreach ( $this->textFormatters as $formatter ) {
+                       $lang = LanguageCode::bcp47( $formatter->getLangCode() );
+                       $messageText = $formatter->format( $messageValue );
+                       $translations[$lang] = $messageText;
+               }
+               return [ 'messageTranslations' => $translations ];
+       }
+
 }
index a520130..6821d89 100644 (file)
@@ -4,6 +4,7 @@ namespace MediaWiki\Rest;
 
 use AppendIterator;
 use BagOStuff;
+use Wikimedia\Message\MessageValue;
 use MediaWiki\Rest\BasicAccess\BasicAuthorizerInterface;
 use MediaWiki\Rest\PathTemplateMatcher\PathMatcher;
 use MediaWiki\Rest\Validator\Validator;
@@ -226,18 +227,28 @@ class Router {
                $path = $request->getUri()->getPath();
                $relPath = $this->getRelativePath( $path );
                if ( $relPath === false ) {
-                       return $this->responseFactory->createHttpError( 404 );
+                       return $this->responseFactory->createLocalizedHttpError( 404,
+                               ( new MessageValue( 'rest-prefix-mismatch' ) )
+                                       ->plaintextParams( $path, $this->rootPath )
+                       );
                }
 
+               $requestMethod = $request->getMethod();
                $matchers = $this->getMatchers();
-               $matcher = $matchers[$request->getMethod()] ?? null;
+               $matcher = $matchers[$requestMethod] ?? null;
                $match = $matcher ? $matcher->match( $relPath ) : null;
 
+               // For a HEAD request, execute the GET handler instead if one exists.
+               // The webserver will discard the body.
+               if ( !$match && $requestMethod === 'HEAD' && isset( $matchers['GET'] ) ) {
+                       $match = $matchers['GET']->match( $relPath );
+               }
+
                if ( !$match ) {
                        // Check for 405 wrong method
                        $allowed = [];
                        foreach ( $matchers as $allowedMethod => $allowedMatcher ) {
-                               if ( $allowedMethod === $request->getMethod() ) {
+                               if ( $allowedMethod === $requestMethod ) {
                                        continue;
                                }
                                if ( $allowedMatcher->match( $relPath ) ) {
@@ -245,12 +256,20 @@ class Router {
                                }
                        }
                        if ( $allowed ) {
-                               $response = $this->responseFactory->createHttpError( 405 );
+                               $response = $this->responseFactory->createLocalizedHttpError( 405,
+                                       ( new MessageValue( 'rest-wrong-method' ) )
+                                               ->textParams( $requestMethod )
+                                               ->commaListParams( $allowed )
+                                               ->numParams( count( $allowed ) )
+                               );
                                $response->setHeader( 'Allow', $allowed );
                                return $response;
                        } else {
                                // Did not match with any other method, must be 404
-                               return $this->responseFactory->createHttpError( 404 );
+                               return $this->responseFactory->createLocalizedHttpError( 404,
+                                       ( new MessageValue( 'rest-no-match' ) )
+                                               ->plaintextParams( $relPath )
+                               );
                        }
                }
 
@@ -272,6 +291,7 @@ class Router {
 
        /**
         * Execute a fully-constructed handler
+        *
         * @param Handler $handler
         * @return ResponseInterface
         */
index 8eecbcc..1475c20 100644 (file)
@@ -54,6 +54,8 @@ class PasswordPolicyChecks {
 
        /**
         * Check password is longer than minimum, fatal.
+        * Intended for locking out users with passwords too short to trust, requiring them
+        * to recover their account by some other means.
         * @param int $policyVal minimal length
         * @param User $user
         * @param string $password
index f5af2c5..69d4fe2 100644 (file)
        "mycustomjsredirectprotected": "You do not have permission to edit this JavaScript page because it is a redirect and it does not point inside your userspace.",
        "easydeflate-invaliddeflate": "Content provided is not properly deflated",
        "unprotected-js": "For security reasons JavaScript cannot be loaded from unprotected pages. Please only create javascript in the MediaWiki: namespace or as a User subpage",
-       "userlogout-continue": "Do you want to log out?"
+       "userlogout-continue": "Do you want to log out?",
+       "rest-prefix-mismatch": "The requested path ($1) was not inside the REST API root path ($2)",
+       "rest-wrong-method": "The request method ($1) was not {{PLURAL:$3|the allowed method for this path|one of the allowed methods for this path}} ($2)",
+       "rest-no-match": "The requested relative path ($1) did not match any known handler"
 }
index a33608f..436c11b 100644 (file)
        "mycustomjsredirectprotected": "Error message shown when user tries to edit their own JS page that is a foreign redirect without the 'mycustomjsredirectprotected' right. See also {{msg-mw|mycustomjsprotected}}.",
        "easydeflate-invaliddeflate": "Error message if the content passed to easydeflate was not deflated (compressed) properly",
        "unprotected-js": "Error message shown when trying to load javascript via action=raw that is not protected",
-       "userlogout-continue": "Shown if user attempted to log out without a token specified. Probably the user clicked on an old link that hasn't been updated to use the new system. $1 - url that user should click on in order to log out."
+       "userlogout-continue": "Shown if user attempted to log out without a token specified. Probably the user clicked on an old link that hasn't been updated to use the new system. $1 - url that user should click on in order to log out.",
+       "rest-prefix-mismatch": "Error message for REST API debugging, shown if $wgRestPath is incorrect or otherwise not matched. Parameters:\n* $1: The requested path.\n* $2: The configured root path ($wgRestPath).",
+       "rest-wrong-method": "Error message for REST API debugging, shown if the HTTP method is incorrect. Parameters:\n* $1: The received request method.\n* $2: A comma-separated list of allowed methods for this path.\n* $3: The number of items in the list $2",
+       "rest-no-match": "Error message for REST API debugging, shown if the path has the correct prefix but did not match any registered handler. Parameters:\n* $1: The received request path, relative to $wgRestPath."
 }
index 5dd9432..e1a50ea 100755 (executable)
@@ -67,7 +67,13 @@ class GeneratePhpCharToUpperMappings extends Maintenance {
                        $phpUpper = $wgContLang->ucfirst( $char );
                        $jsUpper = $jsUpperChars[$i];
                        if ( $jsUpper !== $phpUpper ) {
-                               $data[$char] = $phpUpper;
+                               if ( $char === $phpUpper ) {
+                                       // Optimisation: Use the empty string to signal "leave character unchanged".
+                                       // Reduces the transfer size by ~50%. Reduces browser memory cost as well.
+                                       $data[$char] = '';
+                               } else {
+                                       $data[$char] = $phpUpper;
+                               }
                        }
                }
 
index 3f39fd1..843f00f 100644 (file)
 
 /* Private members */
 
-var
+var toUpperMap,
        mwString = require( 'mediawiki.String' ),
 
-       toUpperMapping = require( './phpCharToUpper.json' ),
-
        namespaceIds = mw.config.get( 'wgNamespaceIds' ),
 
        /**
@@ -765,8 +763,15 @@ Title.normalizeExtension = function ( extension ) {
  * @return {string} Unicode character, in upper case, according to the same rules as in PHP
  */
 Title.phpCharToUpper = function ( chr ) {
-       var mapped = toUpperMapping[ chr ];
-       return mapped || chr.toUpperCase();
+       if ( !toUpperMap ) {
+               toUpperMap = require( './phpCharToUpper.json' );
+       }
+       if ( toUpperMap[ chr ] === '' ) {
+               // Optimisation: When the override is to keep the character unchanged,
+               // we use an empty string in JSON. This reduces the data by 50%.
+               return chr;
+       }
+       return toUpperMap[ chr ] || chr.toUpperCase();
 };
 
 /* Public members */
index 2ba08cf..0334039 100644 (file)
 {
-       "ß": "ß",
-       "ʼn": "ʼn",
-       "ƀ": "ƀ",
-       "ƚ": "ƚ",
-       "Dž": "Dž",
+       "ß": "",
+       "ʼn": "",
+       "ƀ": "",
+       "ƚ": "",
+       "Dž": "",
        "dž": "Dž",
-       "Lj": "Lj",
+       "Lj": "",
        "lj": "Lj",
-       "Nj": "Nj",
+       "Nj": "",
        "nj": "Nj",
-       "ǰ": "ǰ",
-       "Dz": "Dz",
+       "ǰ": "",
+       "Dz": "",
        "dz": "Dz",
-       "ȼ": "ȼ",
-       "ȿ": "ȿ",
-       "ɀ": "ɀ",
-       "ɂ": "ɂ",
-       "ɇ": "ɇ",
-       "ɉ": "ɉ",
-       "ɋ": "ɋ",
-       "ɍ": "ɍ",
-       "ɏ": "ɏ",
-       "ɐ": "ɐ",
-       "ɑ": "ɑ",
-       "ɒ": "ɒ",
-       "ɜ": "ɜ",
-       "ɡ": "ɡ",
-       "ɥ": "ɥ",
-       "ɦ": "ɦ",
-       "ɪ": "ɪ",
-       "ɫ": "ɫ",
-       "ɬ": "ɬ",
-       "ɱ": "ɱ",
-       "ɽ": "ɽ",
-       "ʂ": "ʂ",
-       "ʇ": "ʇ",
-       "ʉ": "ʉ",
-       "ʌ": "ʌ",
-       "ʝ": "ʝ",
-       "ʞ": "ʞ",
-       "ͅ": "ͅ",
-       "ͱ": "ͱ",
-       "ͳ": "ͳ",
-       "ͷ": "ͷ",
-       "ͻ": "ͻ",
-       "ͼ": "ͼ",
-       "ͽ": "ͽ",
-       "ΐ": "ΐ",
-       "ΰ": "ΰ",
-       "ϗ": "ϗ",
+       "ȼ": "",
+       "ȿ": "",
+       "ɀ": "",
+       "ɂ": "",
+       "ɇ": "",
+       "ɉ": "",
+       "ɋ": "",
+       "ɍ": "",
+       "ɏ": "",
+       "ɐ": "",
+       "ɑ": "",
+       "ɒ": "",
+       "ɜ": "",
+       "ɡ": "",
+       "ɥ": "",
+       "ɦ": "",
+       "ɪ": "",
+       "ɫ": "",
+       "ɬ": "",
+       "ɱ": "",
+       "ɽ": "",
+       "ʂ": "",
+       "ʇ": "",
+       "ʉ": "",
+       "ʌ": "",
+       "ʝ": "",
+       "ʞ": "",
+       "ͅ": "",
+       "ͱ": "",
+       "ͳ": "",
+       "ͷ": "",
+       "ͻ": "",
+       "ͼ": "",
+       "ͽ": "",
+       "ΐ": "",
+       "ΰ": "",
+       "ϗ": "",
        "ϲ": "Σ",
-       "ϳ": "ϳ",
-       "ϸ": "ϸ",
-       "ϻ": "ϻ",
-       "ӏ": "ӏ",
-       "ӷ": "ӷ",
-       "ӻ": "ӻ",
-       "ӽ": "ӽ",
-       "ӿ": "ӿ",
-       "ԑ": "ԑ",
-       "ԓ": "ԓ",
-       "ԕ": "ԕ",
-       "ԗ": "ԗ",
-       "ԙ": "ԙ",
-       "ԛ": "ԛ",
-       "ԝ": "ԝ",
-       "ԟ": "ԟ",
-       "ԡ": "ԡ",
-       "ԣ": "ԣ",
-       "ԥ": "ԥ",
-       "ԧ": "ԧ",
-       "ԩ": "ԩ",
-       "ԫ": "ԫ",
-       "ԭ": "ԭ",
-       "ԯ": "ԯ",
-       "և": "և",
-       "ა": "",
-       "ბ": "",
-       "გ": "",
-       "დ": "",
-       "ე": "",
-       "ვ": "",
-       "ზ": "",
-       "თ": "",
-       "ი": "",
-       "კ": "",
-       "ლ": "",
-       "მ": "",
-       "ნ": "",
-       "ო": "",
-       "პ": "",
-       "ჟ": "",
-       "რ": "",
-       "ს": "",
-       "ტ": "",
-       "უ": "",
-       "ფ": "",
-       "ქ": "",
-       "ღ": "",
-       "ყ": "",
-       "შ": "",
-       "ჩ": "",
-       "ც": "",
-       "ძ": "",
-       "წ": "",
-       "ჭ": "",
-       "ხ": "",
-       "ჯ": "",
-       "ჰ": "",
-       "ჱ": "",
-       "ჲ": "",
-       "ჳ": "",
-       "ჴ": "",
-       "ჵ": "",
-       "ჶ": "",
-       "ჷ": "",
-       "ჸ": "",
-       "ჹ": "",
-       "ჺ": "",
-       "ჽ": "",
-       "ჾ": "",
-       "ჿ": "",
-       "ᏸ": "",
-       "ᏹ": "",
-       "ᏺ": "",
-       "ᏻ": "",
-       "ᏼ": "",
-       "ᏽ": "",
-       "ᲀ": "",
-       "ᲁ": "",
-       "ᲂ": "",
-       "ᲃ": "",
-       "ᲄ": "",
-       "ᲅ": "",
-       "ᲆ": "",
-       "ᲇ": "",
-       "ᲈ": "",
-       "ᵹ": "",
-       "ᵽ": "",
-       "ᶎ": "",
-       "ẖ": "",
-       "ẗ": "",
-       "ẘ": "",
-       "ẙ": "",
-       "ẚ": "",
-       "ỻ": "",
-       "ỽ": "",
-       "ỿ": "ỿ",
-       "ὐ": "",
-       "ὒ": "",
-       "ὔ": "",
-       "ὖ": "",
+       "ϳ": "",
+       "ϸ": "",
+       "ϻ": "",
+       "ӏ": "",
+       "ӷ": "",
+       "ӻ": "",
+       "ӽ": "",
+       "ӿ": "",
+       "ԑ": "",
+       "ԓ": "",
+       "ԕ": "",
+       "ԗ": "",
+       "ԙ": "",
+       "ԛ": "",
+       "ԝ": "",
+       "ԟ": "",
+       "ԡ": "",
+       "ԣ": "",
+       "ԥ": "",
+       "ԧ": "",
+       "ԩ": "",
+       "ԫ": "",
+       "ԭ": "",
+       "ԯ": "",
+       "և": "",
+       "ა": "",
+       "ბ": "",
+       "გ": "",
+       "დ": "",
+       "ე": "",
+       "ვ": "",
+       "ზ": "",
+       "თ": "",
+       "ი": "",
+       "კ": "",
+       "ლ": "",
+       "მ": "",
+       "ნ": "",
+       "ო": "",
+       "პ": "",
+       "ჟ": "",
+       "რ": "",
+       "ს": "",
+       "ტ": "",
+       "უ": "",
+       "ფ": "",
+       "ქ": "",
+       "ღ": "",
+       "ყ": "",
+       "შ": "",
+       "ჩ": "",
+       "ც": "",
+       "ძ": "",
+       "წ": "",
+       "ჭ": "",
+       "ხ": "",
+       "ჯ": "",
+       "ჰ": "",
+       "ჱ": "",
+       "ჲ": "",
+       "ჳ": "",
+       "ჴ": "",
+       "ჵ": "",
+       "ჶ": "",
+       "ჷ": "",
+       "ჸ": "",
+       "ჹ": "",
+       "ჺ": "",
+       "ჽ": "",
+       "ჾ": "",
+       "ჿ": "",
+       "ᏸ": "",
+       "ᏹ": "",
+       "ᏺ": "",
+       "ᏻ": "",
+       "ᏼ": "",
+       "ᏽ": "",
+       "ᲀ": "",
+       "ᲁ": "",
+       "ᲂ": "",
+       "ᲃ": "",
+       "ᲄ": "",
+       "ᲅ": "",
+       "ᲆ": "",
+       "ᲇ": "",
+       "ᲈ": "",
+       "ᵹ": "",
+       "ᵽ": "",
+       "ᶎ": "",
+       "ẖ": "",
+       "ẗ": "",
+       "ẘ": "",
+       "ẙ": "",
+       "ẚ": "",
+       "ỻ": "",
+       "ỽ": "",
+       "ỿ": "",
+       "ὐ": "",
+       "ὒ": "",
+       "ὔ": "",
+       "ὖ": "",
        "ᾀ": "ᾈ",
        "ᾁ": "ᾉ",
        "ᾂ": "ᾊ",
        "ᾅ": "ᾍ",
        "ᾆ": "ᾎ",
        "ᾇ": "ᾏ",
-       "ᾈ": "",
-       "ᾉ": "",
-       "ᾊ": "",
-       "ᾋ": "",
-       "ᾌ": "",
-       "ᾍ": "",
-       "ᾎ": "",
-       "ᾏ": "",
+       "ᾈ": "",
+       "ᾉ": "",
+       "ᾊ": "",
+       "ᾋ": "",
+       "ᾌ": "",
+       "ᾍ": "",
+       "ᾎ": "",
+       "ᾏ": "",
        "ᾐ": "ᾘ",
        "ᾑ": "ᾙ",
        "ᾒ": "ᾚ",
        "ᾕ": "ᾝ",
        "ᾖ": "ᾞ",
        "ᾗ": "ᾟ",
-       "ᾘ": "",
-       "ᾙ": "",
-       "ᾚ": "",
-       "ᾛ": "",
-       "ᾜ": "",
-       "ᾝ": "",
-       "ᾞ": "",
-       "ᾟ": "",
+       "ᾘ": "",
+       "ᾙ": "",
+       "ᾚ": "",
+       "ᾛ": "",
+       "ᾜ": "",
+       "ᾝ": "",
+       "ᾞ": "",
+       "ᾟ": "",
        "ᾠ": "ᾨ",
        "ᾡ": "ᾩ",
        "ᾢ": "ᾪ",
        "ᾥ": "ᾭ",
        "ᾦ": "ᾮ",
        "ᾧ": "ᾯ",
-       "ᾨ": "",
-       "ᾩ": "",
-       "ᾪ": "",
-       "ᾫ": "",
-       "ᾬ": "",
-       "ᾭ": "",
-       "ᾮ": "",
-       "ᾯ": "",
-       "ᾲ": "",
+       "ᾨ": "",
+       "ᾩ": "",
+       "ᾪ": "",
+       "ᾫ": "",
+       "ᾬ": "",
+       "ᾭ": "",
+       "ᾮ": "",
+       "ᾯ": "",
+       "ᾲ": "",
        "ᾳ": "ᾼ",
-       "ᾴ": "",
-       "ᾶ": "",
-       "ᾷ": "",
-       "ᾼ": "",
-       "ῂ": "",
+       "ᾴ": "",
+       "ᾶ": "",
+       "ᾷ": "",
+       "ᾼ": "",
+       "ῂ": "",
        "ῃ": "ῌ",
-       "ῄ": "",
-       "ῆ": "",
-       "ῇ": "",
-       "ῌ": "",
-       "ῒ": "",
-       "ΐ": "",
-       "ῖ": "",
-       "ῗ": "",
-       "ῢ": "",
-       "ΰ": "",
-       "ῤ": "",
-       "ῦ": "",
-       "ῧ": "",
-       "ῲ": "",
+       "ῄ": "",
+       "ῆ": "",
+       "ῇ": "",
+       "ῌ": "",
+       "ῒ": "",
+       "ΐ": "",
+       "ῖ": "",
+       "ῗ": "",
+       "ῢ": "",
+       "ΰ": "",
+       "ῤ": "",
+       "ῦ": "",
+       "ῧ": "",
+       "ῲ": "",
        "ῳ": "ῼ",
-       "ῴ": "",
-       "ῶ": "",
-       "ῷ": "",
-       "ῼ": "",
-       "ⅎ": "",
-       "ⅰ": "",
-       "ⅱ": "",
-       "ⅲ": "",
-       "ⅳ": "",
-       "ⅴ": "",
-       "ⅵ": "",
-       "ⅶ": "",
-       "ⅷ": "",
-       "ⅸ": "",
-       "ⅹ": "",
-       "ⅺ": "",
-       "ⅻ": "",
-       "ⅼ": "",
-       "ⅽ": "",
-       "ⅾ": "",
-       "ⅿ": "",
-       "ↄ": "",
-       "ⓐ": "",
-       "ⓑ": "",
-       "ⓒ": "",
-       "ⓓ": "",
-       "ⓔ": "",
-       "ⓕ": "",
-       "ⓖ": "",
-       "ⓗ": "",
-       "ⓘ": "",
-       "ⓙ": "",
-       "ⓚ": "",
-       "ⓛ": "",
-       "ⓜ": "",
-       "ⓝ": "",
-       "ⓞ": "",
-       "ⓟ": "",
-       "ⓠ": "",
-       "ⓡ": "",
-       "ⓢ": "",
-       "ⓣ": "",
-       "ⓤ": "",
-       "ⓥ": "",
-       "ⓦ": "",
-       "ⓧ": "",
-       "ⓨ": "",
-       "ⓩ": "",
-       "ⰰ": "",
-       "ⰱ": "",
-       "ⰲ": "",
-       "ⰳ": "",
-       "ⰴ": "",
-       "ⰵ": "",
-       "ⰶ": "",
-       "ⰷ": "",
-       "ⰸ": "",
-       "ⰹ": "",
-       "ⰺ": "",
-       "ⰻ": "",
-       "ⰼ": "",
-       "ⰽ": "",
-       "ⰾ": "",
-       "ⰿ": "ⰿ",
-       "ⱀ": "",
-       "ⱁ": "",
-       "ⱂ": "",
-       "ⱃ": "",
-       "ⱄ": "",
-       "ⱅ": "",
-       "ⱆ": "",
-       "ⱇ": "",
-       "ⱈ": "",
-       "ⱉ": "",
-       "ⱊ": "",
-       "ⱋ": "",
-       "ⱌ": "",
-       "ⱍ": "",
-       "ⱎ": "",
-       "ⱏ": "",
-       "ⱐ": "",
-       "ⱑ": "",
-       "ⱒ": "",
-       "ⱓ": "",
-       "ⱔ": "",
-       "ⱕ": "",
-       "ⱖ": "",
-       "ⱗ": "",
-       "ⱘ": "",
-       "ⱙ": "",
-       "ⱚ": "",
-       "ⱛ": "",
-       "ⱜ": "",
-       "ⱝ": "",
-       "ⱞ": "",
-       "ⱡ": "",
-       "ⱥ": "",
-       "ⱦ": "",
-       "ⱨ": "",
-       "ⱪ": "",
-       "ⱬ": "",
-       "ⱳ": "",
-       "ⱶ": "",
-       "ⲁ": "",
-       "ⲃ": "",
-       "ⲅ": "",
-       "ⲇ": "",
-       "ⲉ": "",
-       "ⲋ": "",
-       "ⲍ": "",
-       "ⲏ": "",
-       "ⲑ": "",
-       "ⲓ": "",
-       "ⲕ": "",
-       "ⲗ": "",
-       "ⲙ": "",
-       "ⲛ": "",
-       "ⲝ": "",
-       "ⲟ": "",
-       "ⲡ": "",
-       "ⲣ": "",
-       "ⲥ": "",
-       "ⲧ": "",
-       "ⲩ": "",
-       "ⲫ": "",
-       "ⲭ": "",
-       "ⲯ": "",
-       "ⲱ": "",
-       "ⲳ": "",
-       "ⲵ": "",
-       "ⲷ": "",
-       "ⲹ": "",
-       "ⲻ": "",
-       "ⲽ": "",
-       "ⲿ": "ⲿ",
-       "ⳁ": "",
-       "ⳃ": "",
-       "ⳅ": "",
-       "ⳇ": "",
-       "ⳉ": "",
-       "ⳋ": "",
-       "ⳍ": "",
-       "ⳏ": "",
-       "ⳑ": "",
-       "ⳓ": "",
-       "ⳕ": "",
-       "ⳗ": "",
-       "ⳙ": "",
-       "ⳛ": "",
-       "ⳝ": "",
-       "ⳟ": "",
-       "ⳡ": "",
-       "ⳣ": "",
-       "ⳬ": "",
-       "ⳮ": "",
-       "ⳳ": "",
-       "ⴀ": "",
-       "ⴁ": "",
-       "ⴂ": "",
-       "ⴃ": "",
-       "ⴄ": "",
-       "ⴅ": "",
-       "ⴆ": "",
-       "ⴇ": "",
-       "ⴈ": "",
-       "ⴉ": "",
-       "ⴊ": "",
-       "ⴋ": "",
-       "ⴌ": "",
-       "ⴍ": "",
-       "ⴎ": "",
-       "ⴏ": "",
-       "ⴐ": "",
-       "ⴑ": "",
-       "ⴒ": "",
-       "ⴓ": "",
-       "ⴔ": "",
-       "ⴕ": "",
-       "ⴖ": "",
-       "ⴗ": "",
-       "ⴘ": "",
-       "ⴙ": "",
-       "ⴚ": "",
-       "ⴛ": "",
-       "ⴜ": "",
-       "ⴝ": "",
-       "ⴞ": "",
-       "ⴟ": "",
-       "ⴠ": "",
-       "ⴡ": "",
-       "ⴢ": "",
-       "ⴣ": "",
-       "ⴤ": "",
-       "ⴥ": "",
-       "ⴧ": "",
-       "ⴭ": "",
-       "ꙁ": "",
-       "ꙃ": "",
-       "ꙅ": "",
-       "ꙇ": "",
-       "ꙉ": "",
-       "ꙋ": "",
-       "ꙍ": "",
-       "ꙏ": "",
-       "ꙑ": "",
-       "ꙓ": "",
-       "ꙕ": "",
-       "ꙗ": "",
-       "ꙙ": "",
-       "ꙛ": "",
-       "ꙝ": "",
-       "ꙟ": "",
-       "ꙡ": "",
-       "ꙣ": "",
-       "ꙥ": "",
-       "ꙧ": "",
-       "ꙩ": "",
-       "ꙫ": "",
-       "ꙭ": "",
-       "ꚁ": "",
-       "ꚃ": "",
-       "ꚅ": "",
-       "ꚇ": "",
-       "ꚉ": "",
-       "ꚋ": "",
-       "ꚍ": "",
-       "ꚏ": "",
-       "ꚑ": "",
-       "ꚓ": "",
-       "ꚕ": "",
-       "ꚗ": "",
-       "ꚙ": "",
-       "ꚛ": "",
-       "ꜣ": "",
-       "ꜥ": "",
-       "ꜧ": "",
-       "ꜩ": "",
-       "ꜫ": "",
-       "ꜭ": "",
-       "ꜯ": "",
-       "ꜳ": "",
-       "ꜵ": "",
-       "ꜷ": "",
-       "ꜹ": "",
-       "ꜻ": "",
-       "ꜽ": "",
-       "ꜿ": "",
-       "ꝁ": "",
-       "ꝃ": "",
-       "ꝅ": "",
-       "ꝇ": "",
-       "ꝉ": "",
-       "ꝋ": "",
-       "ꝍ": "",
-       "ꝏ": "",
-       "ꝑ": "",
-       "ꝓ": "",
-       "ꝕ": "",
-       "ꝗ": "",
-       "ꝙ": "",
-       "ꝛ": "",
-       "ꝝ": "",
-       "ꝟ": "",
-       "ꝡ": "",
-       "ꝣ": "",
-       "ꝥ": "",
-       "ꝧ": "",
-       "ꝩ": "",
-       "ꝫ": "",
-       "ꝭ": "",
-       "ꝯ": "",
-       "ꝺ": "",
-       "ꝼ": "",
-       "ꝿ": "",
-       "ꞁ": "",
-       "ꞃ": "",
-       "ꞅ": "",
-       "ꞇ": "",
-       "ꞌ": "",
-       "ꞑ": "",
-       "ꞓ": "",
-       "ꞔ": "",
-       "ꞗ": "",
-       "ꞙ": "",
-       "ꞛ": "",
-       "ꞝ": "",
-       "ꞟ": "",
-       "ꞡ": "",
-       "ꞣ": "",
-       "ꞥ": "",
-       "ꞧ": "",
-       "ꞩ": "",
-       "ꞵ": "",
-       "ꞷ": "",
-       "ꞹ": "",
-       "ꞻ": "",
-       "ꞽ": "",
-       "ꞿ": "",
-       "ꟃ": "",
-       "ꭓ": "",
-       "ꭰ": "",
-       "ꭱ": "",
-       "ꭲ": "",
-       "ꭳ": "",
-       "ꭴ": "",
-       "ꭵ": "",
-       "ꭶ": "",
-       "ꭷ": "",
-       "ꭸ": "",
-       "ꭹ": "",
-       "ꭺ": "",
-       "ꭻ": "",
-       "ꭼ": "",
-       "ꭽ": "",
-       "ꭾ": "",
-       "ꭿ": "ꭿ",
-       "ꮀ": "",
-       "ꮁ": "",
-       "ꮂ": "",
-       "ꮃ": "",
-       "ꮄ": "",
-       "ꮅ": "",
-       "ꮆ": "",
-       "ꮇ": "",
-       "ꮈ": "",
-       "ꮉ": "",
-       "ꮊ": "",
-       "ꮋ": "",
-       "ꮌ": "",
-       "ꮍ": "",
-       "ꮎ": "",
-       "ꮏ": "",
-       "ꮐ": "",
-       "ꮑ": "",
-       "ꮒ": "",
-       "ꮓ": "",
-       "ꮔ": "",
-       "ꮕ": "",
-       "ꮖ": "",
-       "ꮗ": "",
-       "ꮘ": "",
-       "ꮙ": "",
-       "ꮚ": "",
-       "ꮛ": "",
-       "ꮜ": "",
-       "ꮝ": "",
-       "ꮞ": "",
-       "ꮟ": "",
-       "ꮠ": "",
-       "ꮡ": "",
-       "ꮢ": "",
-       "ꮣ": "",
-       "ꮤ": "",
-       "ꮥ": "",
-       "ꮦ": "",
-       "ꮧ": "",
-       "ꮨ": "",
-       "ꮩ": "",
-       "ꮪ": "",
-       "ꮫ": "",
-       "ꮬ": "",
-       "ꮭ": "",
-       "ꮮ": "",
-       "ꮯ": "",
-       "ꮰ": "",
-       "ꮱ": "",
-       "ꮲ": "",
-       "ꮳ": "",
-       "ꮴ": "",
-       "ꮵ": "",
-       "ꮶ": "",
-       "ꮷ": "",
-       "ꮸ": "",
-       "ꮹ": "",
-       "ꮺ": "",
-       "ꮻ": "",
-       "ꮼ": "",
-       "ꮽ": "",
-       "ꮾ": "",
-       "ꮿ": "ꮿ",
-       "ff": "",
-       "fi": "",
-       "fl": "",
-       "ffi": "",
-       "ffl": "",
-       "ſt": "",
-       "st": "",
-       "ﬓ": "",
-       "ﬔ": "",
-       "ﬕ": "",
-       "ﬖ": "",
-       "ﬗ": "",
-       "𐑎": "𐑎",
-       "𐑏": "𐑏",
-       "𐓘": "𐓘",
-       "𐓙": "𐓙",
-       "𐓚": "𐓚",
-       "𐓛": "𐓛",
-       "𐓜": "𐓜",
-       "𐓝": "𐓝",
-       "𐓞": "𐓞",
-       "𐓟": "𐓟",
-       "𐓠": "𐓠",
-       "𐓡": "𐓡",
-       "𐓢": "𐓢",
-       "𐓣": "𐓣",
-       "𐓤": "𐓤",
-       "𐓥": "𐓥",
-       "𐓦": "𐓦",
-       "𐓧": "𐓧",
-       "𐓨": "𐓨",
-       "𐓩": "𐓩",
-       "𐓪": "𐓪",
-       "𐓫": "𐓫",
-       "𐓬": "𐓬",
-       "𐓭": "𐓭",
-       "𐓮": "𐓮",
-       "𐓯": "𐓯",
-       "𐓰": "𐓰",
-       "𐓱": "𐓱",
-       "𐓲": "𐓲",
-       "𐓳": "𐓳",
-       "𐓴": "𐓴",
-       "𐓵": "𐓵",
-       "𐓶": "𐓶",
-       "𐓷": "𐓷",
-       "𐓸": "𐓸",
-       "𐓹": "𐓹",
-       "𐓺": "𐓺",
-       "𐓻": "𐓻",
-       "𐳀": "𐳀",
-       "𐳁": "𐳁",
-       "𐳂": "𐳂",
-       "𐳃": "𐳃",
-       "𐳄": "𐳄",
-       "𐳅": "𐳅",
-       "𐳆": "𐳆",
-       "𐳇": "𐳇",
-       "𐳈": "𐳈",
-       "𐳉": "𐳉",
-       "𐳊": "𐳊",
-       "𐳋": "𐳋",
-       "𐳌": "𐳌",
-       "𐳍": "𐳍",
-       "𐳎": "𐳎",
-       "𐳏": "𐳏",
-       "𐳐": "𐳐",
-       "𐳑": "𐳑",
-       "𐳒": "𐳒",
-       "𐳓": "𐳓",
-       "𐳔": "𐳔",
-       "𐳕": "𐳕",
-       "𐳖": "𐳖",
-       "𐳗": "𐳗",
-       "𐳘": "𐳘",
-       "𐳙": "𐳙",
-       "𐳚": "𐳚",
-       "𐳛": "𐳛",
-       "𐳜": "𐳜",
-       "𐳝": "𐳝",
-       "𐳞": "𐳞",
-       "𐳟": "𐳟",
-       "𐳠": "𐳠",
-       "𐳡": "𐳡",
-       "𐳢": "𐳢",
-       "𐳣": "𐳣",
-       "𐳤": "𐳤",
-       "𐳥": "𐳥",
-       "𐳦": "𐳦",
-       "𐳧": "𐳧",
-       "𐳨": "𐳨",
-       "𐳩": "𐳩",
-       "𐳪": "𐳪",
-       "𐳫": "𐳫",
-       "𐳬": "𐳬",
-       "𐳭": "𐳭",
-       "𐳮": "𐳮",
-       "𐳯": "𐳯",
-       "𐳰": "𐳰",
-       "𐳱": "𐳱",
-       "𐳲": "𐳲",
-       "𑣀": "𑣀",
-       "𑣁": "𑣁",
-       "𑣂": "𑣂",
-       "𑣃": "𑣃",
-       "𑣄": "𑣄",
-       "𑣅": "𑣅",
-       "𑣆": "𑣆",
-       "𑣇": "𑣇",
-       "𑣈": "𑣈",
-       "𑣉": "𑣉",
-       "𑣊": "𑣊",
-       "𑣋": "𑣋",
-       "𑣌": "𑣌",
-       "𑣍": "𑣍",
-       "𑣎": "𑣎",
-       "𑣏": "𑣏",
-       "𑣐": "𑣐",
-       "𑣑": "𑣑",
-       "𑣒": "𑣒",
-       "𑣓": "𑣓",
-       "𑣔": "𑣔",
-       "𑣕": "𑣕",
-       "𑣖": "𑣖",
-       "𑣗": "𑣗",
-       "𑣘": "𑣘",
-       "𑣙": "𑣙",
-       "𑣚": "𑣚",
-       "𑣛": "𑣛",
-       "𑣜": "𑣜",
-       "𑣝": "𑣝",
-       "𑣞": "𑣞",
-       "𑣟": "𑣟",
-       "𖹠": "𖹠",
-       "𖹡": "𖹡",
-       "𖹢": "𖹢",
-       "𖹣": "𖹣",
-       "𖹤": "𖹤",
-       "𖹥": "𖹥",
-       "𖹦": "𖹦",
-       "𖹧": "𖹧",
-       "𖹨": "𖹨",
-       "𖹩": "𖹩",
-       "𖹪": "𖹪",
-       "𖹫": "𖹫",
-       "𖹬": "𖹬",
-       "𖹭": "𖹭",
-       "𖹮": "𖹮",
-       "𖹯": "𖹯",
-       "𖹰": "𖹰",
-       "𖹱": "𖹱",
-       "𖹲": "𖹲",
-       "𖹳": "𖹳",
-       "𖹴": "𖹴",
-       "𖹵": "𖹵",
-       "𖹶": "𖹶",
-       "𖹷": "𖹷",
-       "𖹸": "𖹸",
-       "𖹹": "𖹹",
-       "𖹺": "𖹺",
-       "𖹻": "𖹻",
-       "𖹼": "𖹼",
-       "𖹽": "𖹽",
-       "𖹾": "𖹾",
-       "𖹿": "𖹿",
-       "𞤢": "𞤢",
-       "𞤣": "𞤣",
-       "𞤤": "𞤤",
-       "𞤥": "𞤥",
-       "𞤦": "𞤦",
-       "𞤧": "𞤧",
-       "𞤨": "𞤨",
-       "𞤩": "𞤩",
-       "𞤪": "𞤪",
-       "𞤫": "𞤫",
-       "𞤬": "𞤬",
-       "𞤭": "𞤭",
-       "𞤮": "𞤮",
-       "𞤯": "𞤯",
-       "𞤰": "𞤰",
-       "𞤱": "𞤱",
-       "𞤲": "𞤲",
-       "𞤳": "𞤳",
-       "𞤴": "𞤴",
-       "𞤵": "𞤵",
-       "𞤶": "𞤶",
-       "𞤷": "𞤷",
-       "𞤸": "𞤸",
-       "𞤹": "𞤹",
-       "𞤺": "𞤺",
-       "𞤻": "𞤻",
-       "𞤼": "𞤼",
-       "𞤽": "𞤽",
-       "𞤾": "𞤾",
-       "𞤿": "𞤿",
-       "𞥀": "𞥀",
-       "𞥁": "𞥁",
-       "𞥂": "𞥂",
-       "𞥃": "𞥃"
+       "ῴ": "",
+       "ῶ": "",
+       "ῷ": "",
+       "ῼ": "",
+       "ⅎ": "",
+       "ⅰ": "",
+       "ⅱ": "",
+       "ⅲ": "",
+       "ⅳ": "",
+       "ⅴ": "",
+       "ⅵ": "",
+       "ⅶ": "",
+       "ⅷ": "",
+       "ⅸ": "",
+       "ⅹ": "",
+       "ⅺ": "",
+       "ⅻ": "",
+       "ⅼ": "",
+       "ⅽ": "",
+       "ⅾ": "",
+       "ⅿ": "",
+       "ↄ": "",
+       "ⓐ": "",
+       "ⓑ": "",
+       "ⓒ": "",
+       "ⓓ": "",
+       "ⓔ": "",
+       "ⓕ": "",
+       "ⓖ": "",
+       "ⓗ": "",
+       "ⓘ": "",
+       "ⓙ": "",
+       "ⓚ": "",
+       "ⓛ": "",
+       "ⓜ": "",
+       "ⓝ": "",
+       "ⓞ": "",
+       "ⓟ": "",
+       "ⓠ": "",
+       "ⓡ": "",
+       "ⓢ": "",
+       "ⓣ": "",
+       "ⓤ": "",
+       "ⓥ": "",
+       "ⓦ": "",
+       "ⓧ": "",
+       "ⓨ": "",
+       "ⓩ": "",
+       "ⰰ": "",
+       "ⰱ": "",
+       "ⰲ": "",
+       "ⰳ": "",
+       "ⰴ": "",
+       "ⰵ": "",
+       "ⰶ": "",
+       "ⰷ": "",
+       "ⰸ": "",
+       "ⰹ": "",
+       "ⰺ": "",
+       "ⰻ": "",
+       "ⰼ": "",
+       "ⰽ": "",
+       "ⰾ": "",
+       "ⰿ": "",
+       "ⱀ": "",
+       "ⱁ": "",
+       "ⱂ": "",
+       "ⱃ": "",
+       "ⱄ": "",
+       "ⱅ": "",
+       "ⱆ": "",
+       "ⱇ": "",
+       "ⱈ": "",
+       "ⱉ": "",
+       "ⱊ": "",
+       "ⱋ": "",
+       "ⱌ": "",
+       "ⱍ": "",
+       "ⱎ": "",
+       "ⱏ": "",
+       "ⱐ": "",
+       "ⱑ": "",
+       "ⱒ": "",
+       "ⱓ": "",
+       "ⱔ": "",
+       "ⱕ": "",
+       "ⱖ": "",
+       "ⱗ": "",
+       "ⱘ": "",
+       "ⱙ": "",
+       "ⱚ": "",
+       "ⱛ": "",
+       "ⱜ": "",
+       "ⱝ": "",
+       "ⱞ": "",
+       "ⱡ": "",
+       "ⱥ": "",
+       "ⱦ": "",
+       "ⱨ": "",
+       "ⱪ": "",
+       "ⱬ": "",
+       "ⱳ": "",
+       "ⱶ": "",
+       "ⲁ": "",
+       "ⲃ": "",
+       "ⲅ": "",
+       "ⲇ": "",
+       "ⲉ": "",
+       "ⲋ": "",
+       "ⲍ": "",
+       "ⲏ": "",
+       "ⲑ": "",
+       "ⲓ": "",
+       "ⲕ": "",
+       "ⲗ": "",
+       "ⲙ": "",
+       "ⲛ": "",
+       "ⲝ": "",
+       "ⲟ": "",
+       "ⲡ": "",
+       "ⲣ": "",
+       "ⲥ": "",
+       "ⲧ": "",
+       "ⲩ": "",
+       "ⲫ": "",
+       "ⲭ": "",
+       "ⲯ": "",
+       "ⲱ": "",
+       "ⲳ": "",
+       "ⲵ": "",
+       "ⲷ": "",
+       "ⲹ": "",
+       "ⲻ": "",
+       "ⲽ": "",
+       "ⲿ": "",
+       "ⳁ": "",
+       "ⳃ": "",
+       "ⳅ": "",
+       "ⳇ": "",
+       "ⳉ": "",
+       "ⳋ": "",
+       "ⳍ": "",
+       "ⳏ": "",
+       "ⳑ": "",
+       "ⳓ": "",
+       "ⳕ": "",
+       "ⳗ": "",
+       "ⳙ": "",
+       "ⳛ": "",
+       "ⳝ": "",
+       "ⳟ": "",
+       "ⳡ": "",
+       "ⳣ": "",
+       "ⳬ": "",
+       "ⳮ": "",
+       "ⳳ": "",
+       "ⴀ": "",
+       "ⴁ": "",
+       "ⴂ": "",
+       "ⴃ": "",
+       "ⴄ": "",
+       "ⴅ": "",
+       "ⴆ": "",
+       "ⴇ": "",
+       "ⴈ": "",
+       "ⴉ": "",
+       "ⴊ": "",
+       "ⴋ": "",
+       "ⴌ": "",
+       "ⴍ": "",
+       "ⴎ": "",
+       "ⴏ": "",
+       "ⴐ": "",
+       "ⴑ": "",
+       "ⴒ": "",
+       "ⴓ": "",
+       "ⴔ": "",
+       "ⴕ": "",
+       "ⴖ": "",
+       "ⴗ": "",
+       "ⴘ": "",
+       "ⴙ": "",
+       "ⴚ": "",
+       "ⴛ": "",
+       "ⴜ": "",
+       "ⴝ": "",
+       "ⴞ": "",
+       "ⴟ": "",
+       "ⴠ": "",
+       "ⴡ": "",
+       "ⴢ": "",
+       "ⴣ": "",
+       "ⴤ": "",
+       "ⴥ": "",
+       "ⴧ": "",
+       "ⴭ": "",
+       "ꙁ": "",
+       "ꙃ": "",
+       "ꙅ": "",
+       "ꙇ": "",
+       "ꙉ": "",
+       "ꙋ": "",
+       "ꙍ": "",
+       "ꙏ": "",
+       "ꙑ": "",
+       "ꙓ": "",
+       "ꙕ": "",
+       "ꙗ": "",
+       "ꙙ": "",
+       "ꙛ": "",
+       "ꙝ": "",
+       "ꙟ": "",
+       "ꙡ": "",
+       "ꙣ": "",
+       "ꙥ": "",
+       "ꙧ": "",
+       "ꙩ": "",
+       "ꙫ": "",
+       "ꙭ": "",
+       "ꚁ": "",
+       "ꚃ": "",
+       "ꚅ": "",
+       "ꚇ": "",
+       "ꚉ": "",
+       "ꚋ": "",
+       "ꚍ": "",
+       "ꚏ": "",
+       "ꚑ": "",
+       "ꚓ": "",
+       "ꚕ": "",
+       "ꚗ": "",
+       "ꚙ": "",
+       "ꚛ": "",
+       "ꜣ": "",
+       "ꜥ": "",
+       "ꜧ": "",
+       "ꜩ": "",
+       "ꜫ": "",
+       "ꜭ": "",
+       "ꜯ": "",
+       "ꜳ": "",
+       "ꜵ": "",
+       "ꜷ": "",
+       "ꜹ": "",
+       "ꜻ": "",
+       "ꜽ": "",
+       "ꜿ": "",
+       "ꝁ": "",
+       "ꝃ": "",
+       "ꝅ": "",
+       "ꝇ": "",
+       "ꝉ": "",
+       "ꝋ": "",
+       "ꝍ": "",
+       "ꝏ": "",
+       "ꝑ": "",
+       "ꝓ": "",
+       "ꝕ": "",
+       "ꝗ": "",
+       "ꝙ": "",
+       "ꝛ": "",
+       "ꝝ": "",
+       "ꝟ": "",
+       "ꝡ": "",
+       "ꝣ": "",
+       "ꝥ": "",
+       "ꝧ": "",
+       "ꝩ": "",
+       "ꝫ": "",
+       "ꝭ": "",
+       "ꝯ": "",
+       "ꝺ": "",
+       "ꝼ": "",
+       "ꝿ": "",
+       "ꞁ": "",
+       "ꞃ": "",
+       "ꞅ": "",
+       "ꞇ": "",
+       "ꞌ": "",
+       "ꞑ": "",
+       "ꞓ": "",
+       "ꞔ": "",
+       "ꞗ": "",
+       "ꞙ": "",
+       "ꞛ": "",
+       "ꞝ": "",
+       "ꞟ": "",
+       "ꞡ": "",
+       "ꞣ": "",
+       "ꞥ": "",
+       "ꞧ": "",
+       "ꞩ": "",
+       "ꞵ": "",
+       "ꞷ": "",
+       "ꞹ": "",
+       "ꞻ": "",
+       "ꞽ": "",
+       "ꞿ": "",
+       "ꟃ": "",
+       "ꭓ": "",
+       "ꭰ": "",
+       "ꭱ": "",
+       "ꭲ": "",
+       "ꭳ": "",
+       "ꭴ": "",
+       "ꭵ": "",
+       "ꭶ": "",
+       "ꭷ": "",
+       "ꭸ": "",
+       "ꭹ": "",
+       "ꭺ": "",
+       "ꭻ": "",
+       "ꭼ": "",
+       "ꭽ": "",
+       "ꭾ": "",
+       "ꭿ": "",
+       "ꮀ": "",
+       "ꮁ": "",
+       "ꮂ": "",
+       "ꮃ": "",
+       "ꮄ": "",
+       "ꮅ": "",
+       "ꮆ": "",
+       "ꮇ": "",
+       "ꮈ": "",
+       "ꮉ": "",
+       "ꮊ": "",
+       "ꮋ": "",
+       "ꮌ": "",
+       "ꮍ": "",
+       "ꮎ": "",
+       "ꮏ": "",
+       "ꮐ": "",
+       "ꮑ": "",
+       "ꮒ": "",
+       "ꮓ": "",
+       "ꮔ": "",
+       "ꮕ": "",
+       "ꮖ": "",
+       "ꮗ": "",
+       "ꮘ": "",
+       "ꮙ": "",
+       "ꮚ": "",
+       "ꮛ": "",
+       "ꮜ": "",
+       "ꮝ": "",
+       "ꮞ": "",
+       "ꮟ": "",
+       "ꮠ": "",
+       "ꮡ": "",
+       "ꮢ": "",
+       "ꮣ": "",
+       "ꮤ": "",
+       "ꮥ": "",
+       "ꮦ": "",
+       "ꮧ": "",
+       "ꮨ": "",
+       "ꮩ": "",
+       "ꮪ": "",
+       "ꮫ": "",
+       "ꮬ": "",
+       "ꮭ": "",
+       "ꮮ": "",
+       "ꮯ": "",
+       "ꮰ": "",
+       "ꮱ": "",
+       "ꮲ": "",
+       "ꮳ": "",
+       "ꮴ": "",
+       "ꮵ": "",
+       "ꮶ": "",
+       "ꮷ": "",
+       "ꮸ": "",
+       "ꮹ": "",
+       "ꮺ": "",
+       "ꮻ": "",
+       "ꮼ": "",
+       "ꮽ": "",
+       "ꮾ": "",
+       "ꮿ": "",
+       "ff": "",
+       "fi": "",
+       "fl": "",
+       "ffi": "",
+       "ffl": "",
+       "ſt": "",
+       "st": "",
+       "ﬓ": "",
+       "ﬔ": "",
+       "ﬕ": "",
+       "ﬖ": "",
+       "ﬗ": "",
+       "𐑎": "",
+       "𐑏": "",
+       "𐓘": "",
+       "𐓙": "",
+       "𐓚": "",
+       "𐓛": "",
+       "𐓜": "",
+       "𐓝": "",
+       "𐓞": "",
+       "𐓟": "",
+       "𐓠": "",
+       "𐓡": "",
+       "𐓢": "",
+       "𐓣": "",
+       "𐓤": "",
+       "𐓥": "",
+       "𐓦": "",
+       "𐓧": "",
+       "𐓨": "",
+       "𐓩": "",
+       "𐓪": "",
+       "𐓫": "",
+       "𐓬": "",
+       "𐓭": "",
+       "𐓮": "",
+       "𐓯": "",
+       "𐓰": "",
+       "𐓱": "",
+       "𐓲": "",
+       "𐓳": "",
+       "𐓴": "",
+       "𐓵": "",
+       "𐓶": "",
+       "𐓷": "",
+       "𐓸": "",
+       "𐓹": "",
+       "𐓺": "",
+       "𐓻": "",
+       "𐳀": "",
+       "𐳁": "",
+       "𐳂": "",
+       "𐳃": "",
+       "𐳄": "",
+       "𐳅": "",
+       "𐳆": "",
+       "𐳇": "",
+       "𐳈": "",
+       "𐳉": "",
+       "𐳊": "",
+       "𐳋": "",
+       "𐳌": "",
+       "𐳍": "",
+       "𐳎": "",
+       "𐳏": "",
+       "𐳐": "",
+       "𐳑": "",
+       "𐳒": "",
+       "𐳓": "",
+       "𐳔": "",
+       "𐳕": "",
+       "𐳖": "",
+       "𐳗": "",
+       "𐳘": "",
+       "𐳙": "",
+       "𐳚": "",
+       "𐳛": "",
+       "𐳜": "",
+       "𐳝": "",
+       "𐳞": "",
+       "𐳟": "",
+       "𐳠": "",
+       "𐳡": "",
+       "𐳢": "",
+       "𐳣": "",
+       "𐳤": "",
+       "𐳥": "",
+       "𐳦": "",
+       "𐳧": "",
+       "𐳨": "",
+       "𐳩": "",
+       "𐳪": "",
+       "𐳫": "",
+       "𐳬": "",
+       "𐳭": "",
+       "𐳮": "",
+       "𐳯": "",
+       "𐳰": "",
+       "𐳱": "",
+       "𐳲": "",
+       "𑣀": "",
+       "𑣁": "",
+       "𑣂": "",
+       "𑣃": "",
+       "𑣄": "",
+       "𑣅": "",
+       "𑣆": "",
+       "𑣇": "",
+       "𑣈": "",
+       "𑣉": "",
+       "𑣊": "",
+       "𑣋": "",
+       "𑣌": "",
+       "𑣍": "",
+       "𑣎": "",
+       "𑣏": "",
+       "𑣐": "",
+       "𑣑": "",
+       "𑣒": "",
+       "𑣓": "",
+       "𑣔": "",
+       "𑣕": "",
+       "𑣖": "",
+       "𑣗": "",
+       "𑣘": "",
+       "𑣙": "",
+       "𑣚": "",
+       "𑣛": "",
+       "𑣜": "",
+       "𑣝": "",
+       "𑣞": "",
+       "𑣟": "",
+       "𖹠": "",
+       "𖹡": "",
+       "𖹢": "",
+       "𖹣": "",
+       "𖹤": "",
+       "𖹥": "",
+       "𖹦": "",
+       "𖹧": "",
+       "𖹨": "",
+       "𖹩": "",
+       "𖹪": "",
+       "𖹫": "",
+       "𖹬": "",
+       "𖹭": "",
+       "𖹮": "",
+       "𖹯": "",
+       "𖹰": "",
+       "𖹱": "",
+       "𖹲": "",
+       "𖹳": "",
+       "𖹴": "",
+       "𖹵": "",
+       "𖹶": "",
+       "𖹷": "",
+       "𖹸": "",
+       "𖹹": "",
+       "𖹺": "",
+       "𖹻": "",
+       "𖹼": "",
+       "𖹽": "",
+       "𖹾": "",
+       "𖹿": "",
+       "𞤢": "",
+       "𞤣": "",
+       "𞤤": "",
+       "𞤥": "",
+       "𞤦": "",
+       "𞤧": "",
+       "𞤨": "",
+       "𞤩": "",
+       "𞤪": "",
+       "𞤫": "",
+       "𞤬": "",
+       "𞤭": "",
+       "𞤮": "",
+       "𞤯": "",
+       "𞤰": "",
+       "𞤱": "",
+       "𞤲": "",
+       "𞤳": "",
+       "𞤴": "",
+       "𞤵": "",
+       "𞤶": "",
+       "𞤷": "",
+       "𞤸": "",
+       "𞤹": "",
+       "𞤺": "",
+       "𞤻": "",
+       "𞤼": "",
+       "𞤽": "",
+       "𞤾": "",
+       "𞤿": "",
+       "𞥀": "",
+       "𞥁": "",
+       "𞥂": "",
+       "𞥃": ""
 }
index 2d1fd98..a310242 100644 (file)
@@ -46,7 +46,7 @@ class MWBasicRequestAuthorizerTest extends MediaWikiTestCase {
                        [],
                        '/rest',
                        new \EmptyBagOStuff(),
-                       new ResponseFactory(),
+                       new ResponseFactory( [] ),
                        new MWBasicAuthorizer( $user, MediaWikiServices::getInstance()->getPermissionManager() ),
                        $objectFactory,
                        new Validator( $objectFactory, $request, $user )
index b984895..1c9bc41 100644 (file)
@@ -38,7 +38,7 @@ class EntryPointTest extends \MediaWikiTestCase {
                        [],
                        '/rest',
                        new EmptyBagOStuff(),
-                       new ResponseFactory(),
+                       new ResponseFactory( [] ),
                        new StaticBasicAuthorizer(),
                        $objectFactory,
                        new Validator( $objectFactory, $request, new User )
index 91652a2..7d682fd 100644 (file)
@@ -62,7 +62,7 @@ class HelloHandlerTest extends \MediaWikiUnitTestCase {
                        [],
                        '/rest',
                        new EmptyBagOStuff(),
-                       new ResponseFactory(),
+                       new ResponseFactory( [] ),
                        new StaticBasicAuthorizer(),
                        $objectFactory,
                        new Validator( $objectFactory, $request, new User )
index 04d54de..0a98686 100644 (file)
@@ -6,6 +6,8 @@ use ArrayIterator;
 use MediaWiki\Rest\HttpException;
 use MediaWiki\Rest\ResponseFactory;
 use MediaWikiUnitTestCase;
+use Wikimedia\Message\ITextFormatter;
+use Wikimedia\Message\MessageValue;
 
 /** @covers \MediaWiki\Rest\ResponseFactory */
 class ResponseFactoryTest extends MediaWikiUnitTestCase {
@@ -18,14 +20,27 @@ class ResponseFactoryTest extends MediaWikiUnitTestCase {
                ];
        }
 
+       private function createResponseFactory() {
+               $fakeTextFormatter = new class implements ITextFormatter {
+                       function getLangCode() {
+                               return 'qqx';
+                       }
+
+                       function format( MessageValue $message ) {
+                               return $message->getKey();
+                       }
+               };
+               return new ResponseFactory( [ $fakeTextFormatter ] );
+       }
+
        /** @dataProvider provideEncodeJson */
        public function testEncodeJson( $input, $expected ) {
-               $rf = new ResponseFactory;
+               $rf = $this->createResponseFactory();
                $this->assertSame( $expected, $rf->encodeJson( $input ) );
        }
 
        public function testCreateJson() {
-               $rf = new ResponseFactory;
+               $rf = $this->createResponseFactory();
                $response = $rf->createJson( [] );
                $response->getBody()->rewind();
                $this->assertSame( 'application/json', $response->getHeaderLine( 'Content-Type' ) );
@@ -35,7 +50,7 @@ class ResponseFactoryTest extends MediaWikiUnitTestCase {
        }
 
        public function testCreateNoContent() {
-               $rf = new ResponseFactory;
+               $rf = $this->createResponseFactory();
                $response = $rf->createNoContent();
                $this->assertSame( [], $response->getHeader( 'Content-Type' ) );
                $this->assertSame( 0, $response->getBody()->getSize() );
@@ -43,35 +58,35 @@ class ResponseFactoryTest extends MediaWikiUnitTestCase {
        }
 
        public function testCreatePermanentRedirect() {
-               $rf = new ResponseFactory;
+               $rf = $this->createResponseFactory();
                $response = $rf->createPermanentRedirect( 'http://www.example.com/' );
                $this->assertSame( [ 'http://www.example.com/' ], $response->getHeader( 'Location' ) );
                $this->assertSame( 301, $response->getStatusCode() );
        }
 
        public function testCreateLegacyTemporaryRedirect() {
-               $rf = new ResponseFactory;
+               $rf = $this->createResponseFactory();
                $response = $rf->createLegacyTemporaryRedirect( 'http://www.example.com/' );
                $this->assertSame( [ 'http://www.example.com/' ], $response->getHeader( 'Location' ) );
                $this->assertSame( 302, $response->getStatusCode() );
        }
 
        public function testCreateTemporaryRedirect() {
-               $rf = new ResponseFactory;
+               $rf = $this->createResponseFactory();
                $response = $rf->createTemporaryRedirect( 'http://www.example.com/' );
                $this->assertSame( [ 'http://www.example.com/' ], $response->getHeader( 'Location' ) );
                $this->assertSame( 307, $response->getStatusCode() );
        }
 
        public function testCreateSeeOther() {
-               $rf = new ResponseFactory;
+               $rf = $this->createResponseFactory();
                $response = $rf->createSeeOther( 'http://www.example.com/' );
                $this->assertSame( [ 'http://www.example.com/' ], $response->getHeader( 'Location' ) );
                $this->assertSame( 303, $response->getStatusCode() );
        }
 
        public function testCreateNotModified() {
-               $rf = new ResponseFactory;
+               $rf = $this->createResponseFactory();
                $response = $rf->createNotModified();
                $this->assertSame( 0, $response->getBody()->getSize() );
                $this->assertSame( 304, $response->getStatusCode() );
@@ -79,12 +94,12 @@ class ResponseFactoryTest extends MediaWikiUnitTestCase {
 
        /** @expectedException \InvalidArgumentException */
        public function testCreateHttpErrorInvalid() {
-               $rf = new ResponseFactory;
+               $rf = $this->createResponseFactory();
                $rf->createHttpError( 200 );
        }
 
        public function testCreateHttpError() {
-               $rf = new ResponseFactory;
+               $rf = $this->createResponseFactory();
                $response = $rf->createHttpError( 415, [ 'message' => '...' ] );
                $this->assertSame( 415, $response->getStatusCode() );
                $body = $response->getBody();
@@ -95,7 +110,7 @@ class ResponseFactoryTest extends MediaWikiUnitTestCase {
        }
 
        public function testCreateFromExceptionUnlogged() {
-               $rf = new ResponseFactory;
+               $rf = $this->createResponseFactory();
                $response = $rf->createFromException( new HttpException( 'hello', 415 ) );
                $this->assertSame( 415, $response->getStatusCode() );
                $body = $response->getBody();
@@ -106,7 +121,7 @@ class ResponseFactoryTest extends MediaWikiUnitTestCase {
        }
 
        public function testCreateFromExceptionLogged() {
-               $rf = new ResponseFactory;
+               $rf = $this->createResponseFactory();
                $response = $rf->createFromException( new \Exception( "hello", 415 ) );
                $this->assertSame( 500, $response->getStatusCode() );
                $body = $response->getBody();
@@ -131,7 +146,7 @@ class ResponseFactoryTest extends MediaWikiUnitTestCase {
 
        /** @dataProvider provideCreateFromReturnValue */
        public function testCreateFromReturnValue( $input, $expected ) {
-               $rf = new ResponseFactory;
+               $rf = $this->createResponseFactory();
                $response = $rf->createFromReturnValue( $input );
                $body = $response->getBody();
                $body->rewind();
@@ -140,7 +155,17 @@ class ResponseFactoryTest extends MediaWikiUnitTestCase {
 
        /** @expectedException \InvalidArgumentException */
        public function testCreateFromReturnValueInvalid() {
-               $rf = new ResponseFactory;
+               $rf = $this->createResponseFactory();
                $rf->createFromReturnValue( new ArrayIterator );
        }
+
+       public function testCreateLocalizedHttpError() {
+               $rf = $this->createResponseFactory();
+               $response = $rf->createLocalizedHttpError( 404, new MessageValue( 'rftest' ) );
+               $body = $response->getBody();
+               $body->rewind();
+               $this->assertSame(
+                       '{"messageTranslations":{"qqx":"rftest"},"httpCode":404,"httpReason":"Not Found"}',
+                       $body->getContents() );
+       }
 }
index e16ea25..58039ea 100644 (file)
@@ -29,7 +29,7 @@ class RouterTest extends \MediaWikiUnitTestCase {
                        [],
                        '/rest',
                        new \EmptyBagOStuff(),
-                       new ResponseFactory(),
+                       new ResponseFactory( [] ),
                        new StaticBasicAuthorizer( $authError ),
                        $objectFactory,
                        new Validator( $objectFactory, $request, new User )
@@ -55,6 +55,16 @@ class RouterTest extends \MediaWikiUnitTestCase {
                $this->assertSame( 'GET', $response->getHeaderLine( 'Allow' ) );
        }
 
+       public function testHeadToGet() {
+               $request = new RequestData( [
+                       'uri' => new Uri( '/rest/user/joe/hello' ),
+                       'method' => 'HEAD'
+               ] );
+               $router = $this->createRouter( $request );
+               $response = $router->execute( $request );
+               $this->assertSame( 200, $response->getStatusCode() );
+       }
+
        public function testNoMatch() {
                $request = new RequestData( [ 'uri' => new Uri( '/rest/bogus' ) ] );
                $router = $this->createRouter( $request );
index 45c6e62..8a28e4d 100644 (file)
@@ -33,7 +33,7 @@ module.exports = {
         * Shortcut for `MWBot#edit( .. )`.
         * Default username, password and base URL is used unless specified
         *
-        * @since 1.0.0
+        * @since 0.1.0
         * @see <https://www.mediawiki.org/wiki/API:Edit>
         * @param {string} title
         * @param {string} content
@@ -57,7 +57,7 @@ module.exports = {
        /**
         * Shortcut for `MWBot#delete( .. )`.
         *
-        * @since 1.0.0
+        * @since 0.1.0
         * @see <https://www.mediawiki.org/wiki/API:Delete>
         * @param {string} title
         * @param {string} reason
@@ -73,7 +73,7 @@ module.exports = {
        /**
         * Shortcut for `MWBot#request( { acount: 'createaccount', .. } )`.
         *
-        * @since 1.0.0
+        * @since 0.1.0
         * @see <https://www.mediawiki.org/wiki/API:Account_creation>
         * @param {string} username
         * @param {string} password
index dc16e81..357fbd9 100644 (file)
@@ -22,11 +22,11 @@ Utilities to interact with the MediaWiki API. Uses the [mwbot](https://github.co
 Actions are performed logged-in using `browser.options.username` and `browser.options.password`,
 which typically come from `MEDIAWIKI_USER` and `MEDIAWIKI_PASSWORD` environment variables.
 
-* `edit(title, content [, string username [, string password [, string baseUrl ] ] ])`
-* `delete(title, reason)`
-* `createAccount(username, password)`
-* `blockUser(username, expiry)`
-* `unblockUser(username)`
+* `edit(string title, string content [, string username [, string password [, string baseUrl ] ] ])`
+* `delete(string title, string reason)`
+* `createAccount(string username, string password)`
+* `blockUser([ string username [, string expiry ] ])`
+* `unblockUser([ string username ])`
 
 ### RunJobs