* 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.
use RequestContext;
use Title;
use WebResponse;
+use Wikimedia\Message\ITextFormatter;
class EntryPoint {
/** @var RequestInterface */
'cookiePrefix' => $conf->get( 'CookiePrefix' )
] );
+ $responseFactory = new ResponseFactory( self::getTextFormatters( $services ) );
+
// @phan-suppress-next-line PhanAccessMethodInternal
$authorizer = new MWBasicAuthorizer( $context->getUser(),
$services->getPermissionManager() );
ExtensionRegistry::getInstance()->getAttribute( 'RestRoutes' ),
$conf->get( 'RestPath' ),
$services->getLocalServerObjectCache(),
- new ResponseFactory,
+ $responseFactory,
$authorizer,
$objectFactory,
$restValidator
$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
) {
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;
}
}
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
*
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(),
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 ];
+ }
+
}
use AppendIterator;
use BagOStuff;
+use Wikimedia\Message\MessageValue;
use MediaWiki\Rest\BasicAccess\BasicAuthorizerInterface;
use MediaWiki\Rest\PathTemplateMatcher\PathMatcher;
use MediaWiki\Rest\Validator\Validator;
$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 ) ) {
}
}
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 )
+ );
}
}
/**
* Execute a fully-constructed handler
+ *
* @param Handler $handler
* @return ResponseInterface
*/
/**
* 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
"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"
}
"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."
}
$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;
+ }
}
}
/* Private members */
-var
+var toUpperMap,
mwString = require( 'mediawiki.String' ),
- toUpperMapping = require( './phpCharToUpper.json' ),
-
namespaceIds = mw.config.get( 'wgNamespaceIds' ),
/**
* @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 */
{
- "ß": "ß",
- "ʼn": "ʼn",
- "ƀ": "ƀ",
- "ƚ": "ƚ",
- "Dž": "Dž",
+ "ß": "",
+ "ʼn": "",
+ "ƀ": "",
+ "ƚ": "",
+ "Dž": "",
"dž": "Dž",
- "Lj": "Lj",
+ "Lj": "",
"lj": "Lj",
- "Nj": "Nj",
+ "Nj": "",
"nj": "Nj",
- "ǰ": "ǰ",
- "Dz": "Dz",
+ "ǰ": "",
+ "Dz": "",
"dz": "Dz",
- "ȼ": "ȼ",
- "ȿ": "ȿ",
- "ɀ": "ɀ",
- "ɂ": "ɂ",
- "ɇ": "ɇ",
- "ɉ": "ɉ",
- "ɋ": "ɋ",
- "ɍ": "ɍ",
- "ɏ": "ɏ",
- "ɐ": "ɐ",
- "ɑ": "ɑ",
- "ɒ": "ɒ",
- "ɜ": "ɜ",
- "ɡ": "ɡ",
- "ɥ": "ɥ",
- "ɦ": "ɦ",
- "ɪ": "ɪ",
- "ɫ": "ɫ",
- "ɬ": "ɬ",
- "ɱ": "ɱ",
- "ɽ": "ɽ",
- "ʂ": "ʂ",
- "ʇ": "ʇ",
- "ʉ": "ʉ",
- "ʌ": "ʌ",
- "ʝ": "ʝ",
- "ʞ": "ʞ",
- "ͅ": "ͅ",
- "ͱ": "ͱ",
- "ͳ": "ͳ",
- "ͷ": "ͷ",
- "ͻ": "ͻ",
- "ͼ": "ͼ",
- "ͽ": "ͽ",
- "ΐ": "ΐ",
- "ΰ": "ΰ",
- "ϗ": "ϗ",
+ "ȼ": "",
+ "ȿ": "",
+ "ɀ": "",
+ "ɂ": "",
+ "ɇ": "",
+ "ɉ": "",
+ "ɋ": "",
+ "ɍ": "",
+ "ɏ": "",
+ "ɐ": "",
+ "ɑ": "",
+ "ɒ": "",
+ "ɜ": "",
+ "ɡ": "",
+ "ɥ": "",
+ "ɦ": "",
+ "ɪ": "",
+ "ɫ": "",
+ "ɬ": "",
+ "ɱ": "",
+ "ɽ": "",
+ "ʂ": "",
+ "ʇ": "",
+ "ʉ": "",
+ "ʌ": "",
+ "ʝ": "",
+ "ʞ": "",
+ "ͅ": "",
+ "ͱ": "",
+ "ͳ": "",
+ "ͷ": "",
+ "ͻ": "",
+ "ͼ": "",
+ "ͽ": "",
+ "ΐ": "",
+ "ΰ": "",
+ "ϗ": "",
"ϲ": "Σ",
- "ϳ": "ϳ",
- "ϸ": "ϸ",
- "ϻ": "ϻ",
- "ӏ": "ӏ",
- "ӷ": "ӷ",
- "ӻ": "ӻ",
- "ӽ": "ӽ",
- "ӿ": "ӿ",
- "ԑ": "ԑ",
- "ԓ": "ԓ",
- "ԕ": "ԕ",
- "ԗ": "ԗ",
- "ԙ": "ԙ",
- "ԛ": "ԛ",
- "ԝ": "ԝ",
- "ԟ": "ԟ",
- "ԡ": "ԡ",
- "ԣ": "ԣ",
- "ԥ": "ԥ",
- "ԧ": "ԧ",
- "ԩ": "ԩ",
- "ԫ": "ԫ",
- "ԭ": "ԭ",
- "ԯ": "ԯ",
- "և": "և",
- "ა": "ა",
- "ბ": "ბ",
- "გ": "გ",
- "დ": "დ",
- "ე": "ე",
- "ვ": "ვ",
- "ზ": "ზ",
- "თ": "თ",
- "ი": "ი",
- "კ": "კ",
- "ლ": "ლ",
- "მ": "მ",
- "ნ": "ნ",
- "ო": "ო",
- "პ": "პ",
- "ჟ": "ჟ",
- "რ": "რ",
- "ს": "ს",
- "ტ": "ტ",
- "უ": "უ",
- "ფ": "ფ",
- "ქ": "ქ",
- "ღ": "ღ",
- "ყ": "ყ",
- "შ": "შ",
- "ჩ": "ჩ",
- "ც": "ც",
- "ძ": "ძ",
- "წ": "წ",
- "ჭ": "ჭ",
- "ხ": "ხ",
- "ჯ": "ჯ",
- "ჰ": "ჰ",
- "ჱ": "ჱ",
- "ჲ": "ჲ",
- "ჳ": "ჳ",
- "ჴ": "ჴ",
- "ჵ": "ჵ",
- "ჶ": "ჶ",
- "ჷ": "ჷ",
- "ჸ": "ჸ",
- "ჹ": "ჹ",
- "ჺ": "ჺ",
- "ჽ": "ჽ",
- "ჾ": "ჾ",
- "ჿ": "ჿ",
- "ᏸ": "ᏸ",
- "ᏹ": "ᏹ",
- "ᏺ": "ᏺ",
- "ᏻ": "ᏻ",
- "ᏼ": "ᏼ",
- "ᏽ": "ᏽ",
- "ᲀ": "ᲀ",
- "ᲁ": "ᲁ",
- "ᲂ": "ᲂ",
- "ᲃ": "ᲃ",
- "ᲄ": "ᲄ",
- "ᲅ": "ᲅ",
- "ᲆ": "ᲆ",
- "ᲇ": "ᲇ",
- "ᲈ": "ᲈ",
- "ᵹ": "ᵹ",
- "ᵽ": "ᵽ",
- "ᶎ": "ᶎ",
- "ẖ": "ẖ",
- "ẗ": "ẗ",
- "ẘ": "ẘ",
- "ẙ": "ẙ",
- "ẚ": "ẚ",
- "ỻ": "ỻ",
- "ỽ": "ỽ",
- "ỿ": "ỿ",
- "ὐ": "ὐ",
- "ὒ": "ὒ",
- "ὔ": "ὔ",
- "ὖ": "ὖ",
+ "ϳ": "",
+ "ϸ": "",
+ "ϻ": "",
+ "ӏ": "",
+ "ӷ": "",
+ "ӻ": "",
+ "ӽ": "",
+ "ӿ": "",
+ "ԑ": "",
+ "ԓ": "",
+ "ԕ": "",
+ "ԗ": "",
+ "ԙ": "",
+ "ԛ": "",
+ "ԝ": "",
+ "ԟ": "",
+ "ԡ": "",
+ "ԣ": "",
+ "ԥ": "",
+ "ԧ": "",
+ "ԩ": "",
+ "ԫ": "",
+ "ԭ": "",
+ "ԯ": "",
+ "և": "",
+ "ა": "",
+ "ბ": "",
+ "გ": "",
+ "დ": "",
+ "ე": "",
+ "ვ": "",
+ "ზ": "",
+ "თ": "",
+ "ი": "",
+ "კ": "",
+ "ლ": "",
+ "მ": "",
+ "ნ": "",
+ "ო": "",
+ "პ": "",
+ "ჟ": "",
+ "რ": "",
+ "ს": "",
+ "ტ": "",
+ "უ": "",
+ "ფ": "",
+ "ქ": "",
+ "ღ": "",
+ "ყ": "",
+ "შ": "",
+ "ჩ": "",
+ "ც": "",
+ "ძ": "",
+ "წ": "",
+ "ჭ": "",
+ "ხ": "",
+ "ჯ": "",
+ "ჰ": "",
+ "ჱ": "",
+ "ჲ": "",
+ "ჳ": "",
+ "ჴ": "",
+ "ჵ": "",
+ "ჶ": "",
+ "ჷ": "",
+ "ჸ": "",
+ "ჹ": "",
+ "ჺ": "",
+ "ჽ": "",
+ "ჾ": "",
+ "ჿ": "",
+ "ᏸ": "",
+ "ᏹ": "",
+ "ᏺ": "",
+ "ᏻ": "",
+ "ᏼ": "",
+ "ᏽ": "",
+ "ᲀ": "",
+ "ᲁ": "",
+ "ᲂ": "",
+ "ᲃ": "",
+ "ᲄ": "",
+ "ᲅ": "",
+ "ᲆ": "",
+ "ᲇ": "",
+ "ᲈ": "",
+ "ᵹ": "",
+ "ᵽ": "",
+ "ᶎ": "",
+ "ẖ": "",
+ "ẗ": "",
+ "ẘ": "",
+ "ẙ": "",
+ "ẚ": "",
+ "ỻ": "",
+ "ỽ": "",
+ "ỿ": "",
+ "ὐ": "",
+ "ὒ": "",
+ "ὔ": "",
+ "ὖ": "",
"ᾀ": "ᾈ",
"ᾁ": "ᾉ",
"ᾂ": "ᾊ",
"ᾅ": "ᾍ",
"ᾆ": "ᾎ",
"ᾇ": "ᾏ",
- "ᾈ": "ᾈ",
- "ᾉ": "ᾉ",
- "ᾊ": "ᾊ",
- "ᾋ": "ᾋ",
- "ᾌ": "ᾌ",
- "ᾍ": "ᾍ",
- "ᾎ": "ᾎ",
- "ᾏ": "ᾏ",
+ "ᾈ": "",
+ "ᾉ": "",
+ "ᾊ": "",
+ "ᾋ": "",
+ "ᾌ": "",
+ "ᾍ": "",
+ "ᾎ": "",
+ "ᾏ": "",
"ᾐ": "ᾘ",
"ᾑ": "ᾙ",
"ᾒ": "ᾚ",
"ᾕ": "ᾝ",
"ᾖ": "ᾞ",
"ᾗ": "ᾟ",
- "ᾘ": "ᾘ",
- "ᾙ": "ᾙ",
- "ᾚ": "ᾚ",
- "ᾛ": "ᾛ",
- "ᾜ": "ᾜ",
- "ᾝ": "ᾝ",
- "ᾞ": "ᾞ",
- "ᾟ": "ᾟ",
+ "ᾘ": "",
+ "ᾙ": "",
+ "ᾚ": "",
+ "ᾛ": "",
+ "ᾜ": "",
+ "ᾝ": "",
+ "ᾞ": "",
+ "ᾟ": "",
"ᾠ": "ᾨ",
"ᾡ": "ᾩ",
"ᾢ": "ᾪ",
"ᾥ": "ᾭ",
"ᾦ": "ᾮ",
"ᾧ": "ᾯ",
- "ᾨ": "ᾨ",
- "ᾩ": "ᾩ",
- "ᾪ": "ᾪ",
- "ᾫ": "ᾫ",
- "ᾬ": "ᾬ",
- "ᾭ": "ᾭ",
- "ᾮ": "ᾮ",
- "ᾯ": "ᾯ",
- "ᾲ": "ᾲ",
+ "ᾨ": "",
+ "ᾩ": "",
+ "ᾪ": "",
+ "ᾫ": "",
+ "ᾬ": "",
+ "ᾭ": "",
+ "ᾮ": "",
+ "ᾯ": "",
+ "ᾲ": "",
"ᾳ": "ᾼ",
- "ᾴ": "ᾴ",
- "ᾶ": "ᾶ",
- "ᾷ": "ᾷ",
- "ᾼ": "ᾼ",
- "ῂ": "ῂ",
+ "ᾴ": "",
+ "ᾶ": "",
+ "ᾷ": "",
+ "ᾼ": "",
+ "ῂ": "",
"ῃ": "ῌ",
- "ῄ": "ῄ",
- "ῆ": "ῆ",
- "ῇ": "ῇ",
- "ῌ": "ῌ",
- "ῒ": "ῒ",
- "ΐ": "ΐ",
- "ῖ": "ῖ",
- "ῗ": "ῗ",
- "ῢ": "ῢ",
- "ΰ": "ΰ",
- "ῤ": "ῤ",
- "ῦ": "ῦ",
- "ῧ": "ῧ",
- "ῲ": "ῲ",
+ "ῄ": "",
+ "ῆ": "",
+ "ῇ": "",
+ "ῌ": "",
+ "ῒ": "",
+ "ΐ": "",
+ "ῖ": "",
+ "ῗ": "",
+ "ῢ": "",
+ "ΰ": "",
+ "ῤ": "",
+ "ῦ": "",
+ "ῧ": "",
+ "ῲ": "",
"ῳ": "ῼ",
- "ῴ": "ῴ",
- "ῶ": "ῶ",
- "ῷ": "ῷ",
- "ῼ": "ῼ",
- "ⅎ": "ⅎ",
- "ⅰ": "ⅰ",
- "ⅱ": "ⅱ",
- "ⅲ": "ⅲ",
- "ⅳ": "ⅳ",
- "ⅴ": "ⅴ",
- "ⅵ": "ⅵ",
- "ⅶ": "ⅶ",
- "ⅷ": "ⅷ",
- "ⅸ": "ⅸ",
- "ⅹ": "ⅹ",
- "ⅺ": "ⅺ",
- "ⅻ": "ⅻ",
- "ⅼ": "ⅼ",
- "ⅽ": "ⅽ",
- "ⅾ": "ⅾ",
- "ⅿ": "ⅿ",
- "ↄ": "ↄ",
- "ⓐ": "ⓐ",
- "ⓑ": "ⓑ",
- "ⓒ": "ⓒ",
- "ⓓ": "ⓓ",
- "ⓔ": "ⓔ",
- "ⓕ": "ⓕ",
- "ⓖ": "ⓖ",
- "ⓗ": "ⓗ",
- "ⓘ": "ⓘ",
- "ⓙ": "ⓙ",
- "ⓚ": "ⓚ",
- "ⓛ": "ⓛ",
- "ⓜ": "ⓜ",
- "ⓝ": "ⓝ",
- "ⓞ": "ⓞ",
- "ⓟ": "ⓟ",
- "ⓠ": "ⓠ",
- "ⓡ": "ⓡ",
- "ⓢ": "ⓢ",
- "ⓣ": "ⓣ",
- "ⓤ": "ⓤ",
- "ⓥ": "ⓥ",
- "ⓦ": "ⓦ",
- "ⓧ": "ⓧ",
- "ⓨ": "ⓨ",
- "ⓩ": "ⓩ",
- "ⰰ": "ⰰ",
- "ⰱ": "ⰱ",
- "ⰲ": "ⰲ",
- "ⰳ": "ⰳ",
- "ⰴ": "ⰴ",
- "ⰵ": "ⰵ",
- "ⰶ": "ⰶ",
- "ⰷ": "ⰷ",
- "ⰸ": "ⰸ",
- "ⰹ": "ⰹ",
- "ⰺ": "ⰺ",
- "ⰻ": "ⰻ",
- "ⰼ": "ⰼ",
- "ⰽ": "ⰽ",
- "ⰾ": "ⰾ",
- "ⰿ": "ⰿ",
- "ⱀ": "ⱀ",
- "ⱁ": "ⱁ",
- "ⱂ": "ⱂ",
- "ⱃ": "ⱃ",
- "ⱄ": "ⱄ",
- "ⱅ": "ⱅ",
- "ⱆ": "ⱆ",
- "ⱇ": "ⱇ",
- "ⱈ": "ⱈ",
- "ⱉ": "ⱉ",
- "ⱊ": "ⱊ",
- "ⱋ": "ⱋ",
- "ⱌ": "ⱌ",
- "ⱍ": "ⱍ",
- "ⱎ": "ⱎ",
- "ⱏ": "ⱏ",
- "ⱐ": "ⱐ",
- "ⱑ": "ⱑ",
- "ⱒ": "ⱒ",
- "ⱓ": "ⱓ",
- "ⱔ": "ⱔ",
- "ⱕ": "ⱕ",
- "ⱖ": "ⱖ",
- "ⱗ": "ⱗ",
- "ⱘ": "ⱘ",
- "ⱙ": "ⱙ",
- "ⱚ": "ⱚ",
- "ⱛ": "ⱛ",
- "ⱜ": "ⱜ",
- "ⱝ": "ⱝ",
- "ⱞ": "ⱞ",
- "ⱡ": "ⱡ",
- "ⱥ": "ⱥ",
- "ⱦ": "ⱦ",
- "ⱨ": "ⱨ",
- "ⱪ": "ⱪ",
- "ⱬ": "ⱬ",
- "ⱳ": "ⱳ",
- "ⱶ": "ⱶ",
- "ⲁ": "ⲁ",
- "ⲃ": "ⲃ",
- "ⲅ": "ⲅ",
- "ⲇ": "ⲇ",
- "ⲉ": "ⲉ",
- "ⲋ": "ⲋ",
- "ⲍ": "ⲍ",
- "ⲏ": "ⲏ",
- "ⲑ": "ⲑ",
- "ⲓ": "ⲓ",
- "ⲕ": "ⲕ",
- "ⲗ": "ⲗ",
- "ⲙ": "ⲙ",
- "ⲛ": "ⲛ",
- "ⲝ": "ⲝ",
- "ⲟ": "ⲟ",
- "ⲡ": "ⲡ",
- "ⲣ": "ⲣ",
- "ⲥ": "ⲥ",
- "ⲧ": "ⲧ",
- "ⲩ": "ⲩ",
- "ⲫ": "ⲫ",
- "ⲭ": "ⲭ",
- "ⲯ": "ⲯ",
- "ⲱ": "ⲱ",
- "ⲳ": "ⲳ",
- "ⲵ": "ⲵ",
- "ⲷ": "ⲷ",
- "ⲹ": "ⲹ",
- "ⲻ": "ⲻ",
- "ⲽ": "ⲽ",
- "ⲿ": "ⲿ",
- "ⳁ": "ⳁ",
- "ⳃ": "ⳃ",
- "ⳅ": "ⳅ",
- "ⳇ": "ⳇ",
- "ⳉ": "ⳉ",
- "ⳋ": "ⳋ",
- "ⳍ": "ⳍ",
- "ⳏ": "ⳏ",
- "ⳑ": "ⳑ",
- "ⳓ": "ⳓ",
- "ⳕ": "ⳕ",
- "ⳗ": "ⳗ",
- "ⳙ": "ⳙ",
- "ⳛ": "ⳛ",
- "ⳝ": "ⳝ",
- "ⳟ": "ⳟ",
- "ⳡ": "ⳡ",
- "ⳣ": "ⳣ",
- "ⳬ": "ⳬ",
- "ⳮ": "ⳮ",
- "ⳳ": "ⳳ",
- "ⴀ": "ⴀ",
- "ⴁ": "ⴁ",
- "ⴂ": "ⴂ",
- "ⴃ": "ⴃ",
- "ⴄ": "ⴄ",
- "ⴅ": "ⴅ",
- "ⴆ": "ⴆ",
- "ⴇ": "ⴇ",
- "ⴈ": "ⴈ",
- "ⴉ": "ⴉ",
- "ⴊ": "ⴊ",
- "ⴋ": "ⴋ",
- "ⴌ": "ⴌ",
- "ⴍ": "ⴍ",
- "ⴎ": "ⴎ",
- "ⴏ": "ⴏ",
- "ⴐ": "ⴐ",
- "ⴑ": "ⴑ",
- "ⴒ": "ⴒ",
- "ⴓ": "ⴓ",
- "ⴔ": "ⴔ",
- "ⴕ": "ⴕ",
- "ⴖ": "ⴖ",
- "ⴗ": "ⴗ",
- "ⴘ": "ⴘ",
- "ⴙ": "ⴙ",
- "ⴚ": "ⴚ",
- "ⴛ": "ⴛ",
- "ⴜ": "ⴜ",
- "ⴝ": "ⴝ",
- "ⴞ": "ⴞ",
- "ⴟ": "ⴟ",
- "ⴠ": "ⴠ",
- "ⴡ": "ⴡ",
- "ⴢ": "ⴢ",
- "ⴣ": "ⴣ",
- "ⴤ": "ⴤ",
- "ⴥ": "ⴥ",
- "ⴧ": "ⴧ",
- "ⴭ": "ⴭ",
- "ꙁ": "ꙁ",
- "ꙃ": "ꙃ",
- "ꙅ": "ꙅ",
- "ꙇ": "ꙇ",
- "ꙉ": "ꙉ",
- "ꙋ": "ꙋ",
- "ꙍ": "ꙍ",
- "ꙏ": "ꙏ",
- "ꙑ": "ꙑ",
- "ꙓ": "ꙓ",
- "ꙕ": "ꙕ",
- "ꙗ": "ꙗ",
- "ꙙ": "ꙙ",
- "ꙛ": "ꙛ",
- "ꙝ": "ꙝ",
- "ꙟ": "ꙟ",
- "ꙡ": "ꙡ",
- "ꙣ": "ꙣ",
- "ꙥ": "ꙥ",
- "ꙧ": "ꙧ",
- "ꙩ": "ꙩ",
- "ꙫ": "ꙫ",
- "ꙭ": "ꙭ",
- "ꚁ": "ꚁ",
- "ꚃ": "ꚃ",
- "ꚅ": "ꚅ",
- "ꚇ": "ꚇ",
- "ꚉ": "ꚉ",
- "ꚋ": "ꚋ",
- "ꚍ": "ꚍ",
- "ꚏ": "ꚏ",
- "ꚑ": "ꚑ",
- "ꚓ": "ꚓ",
- "ꚕ": "ꚕ",
- "ꚗ": "ꚗ",
- "ꚙ": "ꚙ",
- "ꚛ": "ꚛ",
- "ꜣ": "ꜣ",
- "ꜥ": "ꜥ",
- "ꜧ": "ꜧ",
- "ꜩ": "ꜩ",
- "ꜫ": "ꜫ",
- "ꜭ": "ꜭ",
- "ꜯ": "ꜯ",
- "ꜳ": "ꜳ",
- "ꜵ": "ꜵ",
- "ꜷ": "ꜷ",
- "ꜹ": "ꜹ",
- "ꜻ": "ꜻ",
- "ꜽ": "ꜽ",
- "ꜿ": "ꜿ",
- "ꝁ": "ꝁ",
- "ꝃ": "ꝃ",
- "ꝅ": "ꝅ",
- "ꝇ": "ꝇ",
- "ꝉ": "ꝉ",
- "ꝋ": "ꝋ",
- "ꝍ": "ꝍ",
- "ꝏ": "ꝏ",
- "ꝑ": "ꝑ",
- "ꝓ": "ꝓ",
- "ꝕ": "ꝕ",
- "ꝗ": "ꝗ",
- "ꝙ": "ꝙ",
- "ꝛ": "ꝛ",
- "ꝝ": "ꝝ",
- "ꝟ": "ꝟ",
- "ꝡ": "ꝡ",
- "ꝣ": "ꝣ",
- "ꝥ": "ꝥ",
- "ꝧ": "ꝧ",
- "ꝩ": "ꝩ",
- "ꝫ": "ꝫ",
- "ꝭ": "ꝭ",
- "ꝯ": "ꝯ",
- "ꝺ": "ꝺ",
- "ꝼ": "ꝼ",
- "ꝿ": "ꝿ",
- "ꞁ": "ꞁ",
- "ꞃ": "ꞃ",
- "ꞅ": "ꞅ",
- "ꞇ": "ꞇ",
- "ꞌ": "ꞌ",
- "ꞑ": "ꞑ",
- "ꞓ": "ꞓ",
- "ꞔ": "ꞔ",
- "ꞗ": "ꞗ",
- "ꞙ": "ꞙ",
- "ꞛ": "ꞛ",
- "ꞝ": "ꞝ",
- "ꞟ": "ꞟ",
- "ꞡ": "ꞡ",
- "ꞣ": "ꞣ",
- "ꞥ": "ꞥ",
- "ꞧ": "ꞧ",
- "ꞩ": "ꞩ",
- "ꞵ": "ꞵ",
- "ꞷ": "ꞷ",
- "ꞹ": "ꞹ",
- "ꞻ": "ꞻ",
- "ꞽ": "ꞽ",
- "ꞿ": "ꞿ",
- "ꟃ": "ꟃ",
- "ꭓ": "ꭓ",
- "ꭰ": "ꭰ",
- "ꭱ": "ꭱ",
- "ꭲ": "ꭲ",
- "ꭳ": "ꭳ",
- "ꭴ": "ꭴ",
- "ꭵ": "ꭵ",
- "ꭶ": "ꭶ",
- "ꭷ": "ꭷ",
- "ꭸ": "ꭸ",
- "ꭹ": "ꭹ",
- "ꭺ": "ꭺ",
- "ꭻ": "ꭻ",
- "ꭼ": "ꭼ",
- "ꭽ": "ꭽ",
- "ꭾ": "ꭾ",
- "ꭿ": "ꭿ",
- "ꮀ": "ꮀ",
- "ꮁ": "ꮁ",
- "ꮂ": "ꮂ",
- "ꮃ": "ꮃ",
- "ꮄ": "ꮄ",
- "ꮅ": "ꮅ",
- "ꮆ": "ꮆ",
- "ꮇ": "ꮇ",
- "ꮈ": "ꮈ",
- "ꮉ": "ꮉ",
- "ꮊ": "ꮊ",
- "ꮋ": "ꮋ",
- "ꮌ": "ꮌ",
- "ꮍ": "ꮍ",
- "ꮎ": "ꮎ",
- "ꮏ": "ꮏ",
- "ꮐ": "ꮐ",
- "ꮑ": "ꮑ",
- "ꮒ": "ꮒ",
- "ꮓ": "ꮓ",
- "ꮔ": "ꮔ",
- "ꮕ": "ꮕ",
- "ꮖ": "ꮖ",
- "ꮗ": "ꮗ",
- "ꮘ": "ꮘ",
- "ꮙ": "ꮙ",
- "ꮚ": "ꮚ",
- "ꮛ": "ꮛ",
- "ꮜ": "ꮜ",
- "ꮝ": "ꮝ",
- "ꮞ": "ꮞ",
- "ꮟ": "ꮟ",
- "ꮠ": "ꮠ",
- "ꮡ": "ꮡ",
- "ꮢ": "ꮢ",
- "ꮣ": "ꮣ",
- "ꮤ": "ꮤ",
- "ꮥ": "ꮥ",
- "ꮦ": "ꮦ",
- "ꮧ": "ꮧ",
- "ꮨ": "ꮨ",
- "ꮩ": "ꮩ",
- "ꮪ": "ꮪ",
- "ꮫ": "ꮫ",
- "ꮬ": "ꮬ",
- "ꮭ": "ꮭ",
- "ꮮ": "ꮮ",
- "ꮯ": "ꮯ",
- "ꮰ": "ꮰ",
- "ꮱ": "ꮱ",
- "ꮲ": "ꮲ",
- "ꮳ": "ꮳ",
- "ꮴ": "ꮴ",
- "ꮵ": "ꮵ",
- "ꮶ": "ꮶ",
- "ꮷ": "ꮷ",
- "ꮸ": "ꮸ",
- "ꮹ": "ꮹ",
- "ꮺ": "ꮺ",
- "ꮻ": "ꮻ",
- "ꮼ": "ꮼ",
- "ꮽ": "ꮽ",
- "ꮾ": "ꮾ",
- "ꮿ": "ꮿ",
- "ff": "ff",
- "fi": "fi",
- "fl": "fl",
- "ffi": "ffi",
- "ffl": "ffl",
- "ſt": "ſt",
- "st": "st",
- "ﬓ": "ﬓ",
- "ﬔ": "ﬔ",
- "ﬕ": "ﬕ",
- "ﬖ": "ﬖ",
- "ﬗ": "ﬗ",
- "𐑎": "𐑎",
- "𐑏": "𐑏",
- "𐓘": "𐓘",
- "𐓙": "𐓙",
- "𐓚": "𐓚",
- "𐓛": "𐓛",
- "𐓜": "𐓜",
- "𐓝": "𐓝",
- "𐓞": "𐓞",
- "𐓟": "𐓟",
- "𐓠": "𐓠",
- "𐓡": "𐓡",
- "𐓢": "𐓢",
- "𐓣": "𐓣",
- "𐓤": "𐓤",
- "𐓥": "𐓥",
- "𐓦": "𐓦",
- "𐓧": "𐓧",
- "𐓨": "𐓨",
- "𐓩": "𐓩",
- "𐓪": "𐓪",
- "𐓫": "𐓫",
- "𐓬": "𐓬",
- "𐓭": "𐓭",
- "𐓮": "𐓮",
- "𐓯": "𐓯",
- "𐓰": "𐓰",
- "𐓱": "𐓱",
- "𐓲": "𐓲",
- "𐓳": "𐓳",
- "𐓴": "𐓴",
- "𐓵": "𐓵",
- "𐓶": "𐓶",
- "𐓷": "𐓷",
- "𐓸": "𐓸",
- "𐓹": "𐓹",
- "𐓺": "𐓺",
- "𐓻": "𐓻",
- "𐳀": "𐳀",
- "𐳁": "𐳁",
- "𐳂": "𐳂",
- "𐳃": "𐳃",
- "𐳄": "𐳄",
- "𐳅": "𐳅",
- "𐳆": "𐳆",
- "𐳇": "𐳇",
- "𐳈": "𐳈",
- "𐳉": "𐳉",
- "𐳊": "𐳊",
- "𐳋": "𐳋",
- "𐳌": "𐳌",
- "𐳍": "𐳍",
- "𐳎": "𐳎",
- "𐳏": "𐳏",
- "𐳐": "𐳐",
- "𐳑": "𐳑",
- "𐳒": "𐳒",
- "𐳓": "𐳓",
- "𐳔": "𐳔",
- "𐳕": "𐳕",
- "𐳖": "𐳖",
- "𐳗": "𐳗",
- "𐳘": "𐳘",
- "𐳙": "𐳙",
- "𐳚": "𐳚",
- "𐳛": "𐳛",
- "𐳜": "𐳜",
- "𐳝": "𐳝",
- "𐳞": "𐳞",
- "𐳟": "𐳟",
- "𐳠": "𐳠",
- "𐳡": "𐳡",
- "𐳢": "𐳢",
- "𐳣": "𐳣",
- "𐳤": "𐳤",
- "𐳥": "𐳥",
- "𐳦": "𐳦",
- "𐳧": "𐳧",
- "𐳨": "𐳨",
- "𐳩": "𐳩",
- "𐳪": "𐳪",
- "𐳫": "𐳫",
- "𐳬": "𐳬",
- "𐳭": "𐳭",
- "𐳮": "𐳮",
- "𐳯": "𐳯",
- "𐳰": "𐳰",
- "𐳱": "𐳱",
- "𐳲": "𐳲",
- "𑣀": "𑣀",
- "𑣁": "𑣁",
- "𑣂": "𑣂",
- "𑣃": "𑣃",
- "𑣄": "𑣄",
- "𑣅": "𑣅",
- "𑣆": "𑣆",
- "𑣇": "𑣇",
- "𑣈": "𑣈",
- "𑣉": "𑣉",
- "𑣊": "𑣊",
- "𑣋": "𑣋",
- "𑣌": "𑣌",
- "𑣍": "𑣍",
- "𑣎": "𑣎",
- "𑣏": "𑣏",
- "𑣐": "𑣐",
- "𑣑": "𑣑",
- "𑣒": "𑣒",
- "𑣓": "𑣓",
- "𑣔": "𑣔",
- "𑣕": "𑣕",
- "𑣖": "𑣖",
- "𑣗": "𑣗",
- "𑣘": "𑣘",
- "𑣙": "𑣙",
- "𑣚": "𑣚",
- "𑣛": "𑣛",
- "𑣜": "𑣜",
- "𑣝": "𑣝",
- "𑣞": "𑣞",
- "𑣟": "𑣟",
- "𖹠": "𖹠",
- "𖹡": "𖹡",
- "𖹢": "𖹢",
- "𖹣": "𖹣",
- "𖹤": "𖹤",
- "𖹥": "𖹥",
- "𖹦": "𖹦",
- "𖹧": "𖹧",
- "𖹨": "𖹨",
- "𖹩": "𖹩",
- "𖹪": "𖹪",
- "𖹫": "𖹫",
- "𖹬": "𖹬",
- "𖹭": "𖹭",
- "𖹮": "𖹮",
- "𖹯": "𖹯",
- "𖹰": "𖹰",
- "𖹱": "𖹱",
- "𖹲": "𖹲",
- "𖹳": "𖹳",
- "𖹴": "𖹴",
- "𖹵": "𖹵",
- "𖹶": "𖹶",
- "𖹷": "𖹷",
- "𖹸": "𖹸",
- "𖹹": "𖹹",
- "𖹺": "𖹺",
- "𖹻": "𖹻",
- "𖹼": "𖹼",
- "𖹽": "𖹽",
- "𖹾": "𖹾",
- "𖹿": "𖹿",
- "𞤢": "𞤢",
- "𞤣": "𞤣",
- "𞤤": "𞤤",
- "𞤥": "𞤥",
- "𞤦": "𞤦",
- "𞤧": "𞤧",
- "𞤨": "𞤨",
- "𞤩": "𞤩",
- "𞤪": "𞤪",
- "𞤫": "𞤫",
- "𞤬": "𞤬",
- "𞤭": "𞤭",
- "𞤮": "𞤮",
- "𞤯": "𞤯",
- "𞤰": "𞤰",
- "𞤱": "𞤱",
- "𞤲": "𞤲",
- "𞤳": "𞤳",
- "𞤴": "𞤴",
- "𞤵": "𞤵",
- "𞤶": "𞤶",
- "𞤷": "𞤷",
- "𞤸": "𞤸",
- "𞤹": "𞤹",
- "𞤺": "𞤺",
- "𞤻": "𞤻",
- "𞤼": "𞤼",
- "𞤽": "𞤽",
- "𞤾": "𞤾",
- "𞤿": "𞤿",
- "𞥀": "𞥀",
- "𞥁": "𞥁",
- "𞥂": "𞥂",
- "𞥃": "𞥃"
+ "ῴ": "",
+ "ῶ": "",
+ "ῷ": "",
+ "ῼ": "",
+ "ⅎ": "",
+ "ⅰ": "",
+ "ⅱ": "",
+ "ⅲ": "",
+ "ⅳ": "",
+ "ⅴ": "",
+ "ⅵ": "",
+ "ⅶ": "",
+ "ⅷ": "",
+ "ⅸ": "",
+ "ⅹ": "",
+ "ⅺ": "",
+ "ⅻ": "",
+ "ⅼ": "",
+ "ⅽ": "",
+ "ⅾ": "",
+ "ⅿ": "",
+ "ↄ": "",
+ "ⓐ": "",
+ "ⓑ": "",
+ "ⓒ": "",
+ "ⓓ": "",
+ "ⓔ": "",
+ "ⓕ": "",
+ "ⓖ": "",
+ "ⓗ": "",
+ "ⓘ": "",
+ "ⓙ": "",
+ "ⓚ": "",
+ "ⓛ": "",
+ "ⓜ": "",
+ "ⓝ": "",
+ "ⓞ": "",
+ "ⓟ": "",
+ "ⓠ": "",
+ "ⓡ": "",
+ "ⓢ": "",
+ "ⓣ": "",
+ "ⓤ": "",
+ "ⓥ": "",
+ "ⓦ": "",
+ "ⓧ": "",
+ "ⓨ": "",
+ "ⓩ": "",
+ "ⰰ": "",
+ "ⰱ": "",
+ "ⰲ": "",
+ "ⰳ": "",
+ "ⰴ": "",
+ "ⰵ": "",
+ "ⰶ": "",
+ "ⰷ": "",
+ "ⰸ": "",
+ "ⰹ": "",
+ "ⰺ": "",
+ "ⰻ": "",
+ "ⰼ": "",
+ "ⰽ": "",
+ "ⰾ": "",
+ "ⰿ": "",
+ "ⱀ": "",
+ "ⱁ": "",
+ "ⱂ": "",
+ "ⱃ": "",
+ "ⱄ": "",
+ "ⱅ": "",
+ "ⱆ": "",
+ "ⱇ": "",
+ "ⱈ": "",
+ "ⱉ": "",
+ "ⱊ": "",
+ "ⱋ": "",
+ "ⱌ": "",
+ "ⱍ": "",
+ "ⱎ": "",
+ "ⱏ": "",
+ "ⱐ": "",
+ "ⱑ": "",
+ "ⱒ": "",
+ "ⱓ": "",
+ "ⱔ": "",
+ "ⱕ": "",
+ "ⱖ": "",
+ "ⱗ": "",
+ "ⱘ": "",
+ "ⱙ": "",
+ "ⱚ": "",
+ "ⱛ": "",
+ "ⱜ": "",
+ "ⱝ": "",
+ "ⱞ": "",
+ "ⱡ": "",
+ "ⱥ": "",
+ "ⱦ": "",
+ "ⱨ": "",
+ "ⱪ": "",
+ "ⱬ": "",
+ "ⱳ": "",
+ "ⱶ": "",
+ "ⲁ": "",
+ "ⲃ": "",
+ "ⲅ": "",
+ "ⲇ": "",
+ "ⲉ": "",
+ "ⲋ": "",
+ "ⲍ": "",
+ "ⲏ": "",
+ "ⲑ": "",
+ "ⲓ": "",
+ "ⲕ": "",
+ "ⲗ": "",
+ "ⲙ": "",
+ "ⲛ": "",
+ "ⲝ": "",
+ "ⲟ": "",
+ "ⲡ": "",
+ "ⲣ": "",
+ "ⲥ": "",
+ "ⲧ": "",
+ "ⲩ": "",
+ "ⲫ": "",
+ "ⲭ": "",
+ "ⲯ": "",
+ "ⲱ": "",
+ "ⲳ": "",
+ "ⲵ": "",
+ "ⲷ": "",
+ "ⲹ": "",
+ "ⲻ": "",
+ "ⲽ": "",
+ "ⲿ": "",
+ "ⳁ": "",
+ "ⳃ": "",
+ "ⳅ": "",
+ "ⳇ": "",
+ "ⳉ": "",
+ "ⳋ": "",
+ "ⳍ": "",
+ "ⳏ": "",
+ "ⳑ": "",
+ "ⳓ": "",
+ "ⳕ": "",
+ "ⳗ": "",
+ "ⳙ": "",
+ "ⳛ": "",
+ "ⳝ": "",
+ "ⳟ": "",
+ "ⳡ": "",
+ "ⳣ": "",
+ "ⳬ": "",
+ "ⳮ": "",
+ "ⳳ": "",
+ "ⴀ": "",
+ "ⴁ": "",
+ "ⴂ": "",
+ "ⴃ": "",
+ "ⴄ": "",
+ "ⴅ": "",
+ "ⴆ": "",
+ "ⴇ": "",
+ "ⴈ": "",
+ "ⴉ": "",
+ "ⴊ": "",
+ "ⴋ": "",
+ "ⴌ": "",
+ "ⴍ": "",
+ "ⴎ": "",
+ "ⴏ": "",
+ "ⴐ": "",
+ "ⴑ": "",
+ "ⴒ": "",
+ "ⴓ": "",
+ "ⴔ": "",
+ "ⴕ": "",
+ "ⴖ": "",
+ "ⴗ": "",
+ "ⴘ": "",
+ "ⴙ": "",
+ "ⴚ": "",
+ "ⴛ": "",
+ "ⴜ": "",
+ "ⴝ": "",
+ "ⴞ": "",
+ "ⴟ": "",
+ "ⴠ": "",
+ "ⴡ": "",
+ "ⴢ": "",
+ "ⴣ": "",
+ "ⴤ": "",
+ "ⴥ": "",
+ "ⴧ": "",
+ "ⴭ": "",
+ "ꙁ": "",
+ "ꙃ": "",
+ "ꙅ": "",
+ "ꙇ": "",
+ "ꙉ": "",
+ "ꙋ": "",
+ "ꙍ": "",
+ "ꙏ": "",
+ "ꙑ": "",
+ "ꙓ": "",
+ "ꙕ": "",
+ "ꙗ": "",
+ "ꙙ": "",
+ "ꙛ": "",
+ "ꙝ": "",
+ "ꙟ": "",
+ "ꙡ": "",
+ "ꙣ": "",
+ "ꙥ": "",
+ "ꙧ": "",
+ "ꙩ": "",
+ "ꙫ": "",
+ "ꙭ": "",
+ "ꚁ": "",
+ "ꚃ": "",
+ "ꚅ": "",
+ "ꚇ": "",
+ "ꚉ": "",
+ "ꚋ": "",
+ "ꚍ": "",
+ "ꚏ": "",
+ "ꚑ": "",
+ "ꚓ": "",
+ "ꚕ": "",
+ "ꚗ": "",
+ "ꚙ": "",
+ "ꚛ": "",
+ "ꜣ": "",
+ "ꜥ": "",
+ "ꜧ": "",
+ "ꜩ": "",
+ "ꜫ": "",
+ "ꜭ": "",
+ "ꜯ": "",
+ "ꜳ": "",
+ "ꜵ": "",
+ "ꜷ": "",
+ "ꜹ": "",
+ "ꜻ": "",
+ "ꜽ": "",
+ "ꜿ": "",
+ "ꝁ": "",
+ "ꝃ": "",
+ "ꝅ": "",
+ "ꝇ": "",
+ "ꝉ": "",
+ "ꝋ": "",
+ "ꝍ": "",
+ "ꝏ": "",
+ "ꝑ": "",
+ "ꝓ": "",
+ "ꝕ": "",
+ "ꝗ": "",
+ "ꝙ": "",
+ "ꝛ": "",
+ "ꝝ": "",
+ "ꝟ": "",
+ "ꝡ": "",
+ "ꝣ": "",
+ "ꝥ": "",
+ "ꝧ": "",
+ "ꝩ": "",
+ "ꝫ": "",
+ "ꝭ": "",
+ "ꝯ": "",
+ "ꝺ": "",
+ "ꝼ": "",
+ "ꝿ": "",
+ "ꞁ": "",
+ "ꞃ": "",
+ "ꞅ": "",
+ "ꞇ": "",
+ "ꞌ": "",
+ "ꞑ": "",
+ "ꞓ": "",
+ "ꞔ": "",
+ "ꞗ": "",
+ "ꞙ": "",
+ "ꞛ": "",
+ "ꞝ": "",
+ "ꞟ": "",
+ "ꞡ": "",
+ "ꞣ": "",
+ "ꞥ": "",
+ "ꞧ": "",
+ "ꞩ": "",
+ "ꞵ": "",
+ "ꞷ": "",
+ "ꞹ": "",
+ "ꞻ": "",
+ "ꞽ": "",
+ "ꞿ": "",
+ "ꟃ": "",
+ "ꭓ": "",
+ "ꭰ": "",
+ "ꭱ": "",
+ "ꭲ": "",
+ "ꭳ": "",
+ "ꭴ": "",
+ "ꭵ": "",
+ "ꭶ": "",
+ "ꭷ": "",
+ "ꭸ": "",
+ "ꭹ": "",
+ "ꭺ": "",
+ "ꭻ": "",
+ "ꭼ": "",
+ "ꭽ": "",
+ "ꭾ": "",
+ "ꭿ": "",
+ "ꮀ": "",
+ "ꮁ": "",
+ "ꮂ": "",
+ "ꮃ": "",
+ "ꮄ": "",
+ "ꮅ": "",
+ "ꮆ": "",
+ "ꮇ": "",
+ "ꮈ": "",
+ "ꮉ": "",
+ "ꮊ": "",
+ "ꮋ": "",
+ "ꮌ": "",
+ "ꮍ": "",
+ "ꮎ": "",
+ "ꮏ": "",
+ "ꮐ": "",
+ "ꮑ": "",
+ "ꮒ": "",
+ "ꮓ": "",
+ "ꮔ": "",
+ "ꮕ": "",
+ "ꮖ": "",
+ "ꮗ": "",
+ "ꮘ": "",
+ "ꮙ": "",
+ "ꮚ": "",
+ "ꮛ": "",
+ "ꮜ": "",
+ "ꮝ": "",
+ "ꮞ": "",
+ "ꮟ": "",
+ "ꮠ": "",
+ "ꮡ": "",
+ "ꮢ": "",
+ "ꮣ": "",
+ "ꮤ": "",
+ "ꮥ": "",
+ "ꮦ": "",
+ "ꮧ": "",
+ "ꮨ": "",
+ "ꮩ": "",
+ "ꮪ": "",
+ "ꮫ": "",
+ "ꮬ": "",
+ "ꮭ": "",
+ "ꮮ": "",
+ "ꮯ": "",
+ "ꮰ": "",
+ "ꮱ": "",
+ "ꮲ": "",
+ "ꮳ": "",
+ "ꮴ": "",
+ "ꮵ": "",
+ "ꮶ": "",
+ "ꮷ": "",
+ "ꮸ": "",
+ "ꮹ": "",
+ "ꮺ": "",
+ "ꮻ": "",
+ "ꮼ": "",
+ "ꮽ": "",
+ "ꮾ": "",
+ "ꮿ": "",
+ "ff": "",
+ "fi": "",
+ "fl": "",
+ "ffi": "",
+ "ffl": "",
+ "ſt": "",
+ "st": "",
+ "ﬓ": "",
+ "ﬔ": "",
+ "ﬕ": "",
+ "ﬖ": "",
+ "ﬗ": "",
+ "𐑎": "",
+ "𐑏": "",
+ "𐓘": "",
+ "𐓙": "",
+ "𐓚": "",
+ "𐓛": "",
+ "𐓜": "",
+ "𐓝": "",
+ "𐓞": "",
+ "𐓟": "",
+ "𐓠": "",
+ "𐓡": "",
+ "𐓢": "",
+ "𐓣": "",
+ "𐓤": "",
+ "𐓥": "",
+ "𐓦": "",
+ "𐓧": "",
+ "𐓨": "",
+ "𐓩": "",
+ "𐓪": "",
+ "𐓫": "",
+ "𐓬": "",
+ "𐓭": "",
+ "𐓮": "",
+ "𐓯": "",
+ "𐓰": "",
+ "𐓱": "",
+ "𐓲": "",
+ "𐓳": "",
+ "𐓴": "",
+ "𐓵": "",
+ "𐓶": "",
+ "𐓷": "",
+ "𐓸": "",
+ "𐓹": "",
+ "𐓺": "",
+ "𐓻": "",
+ "𐳀": "",
+ "𐳁": "",
+ "𐳂": "",
+ "𐳃": "",
+ "𐳄": "",
+ "𐳅": "",
+ "𐳆": "",
+ "𐳇": "",
+ "𐳈": "",
+ "𐳉": "",
+ "𐳊": "",
+ "𐳋": "",
+ "𐳌": "",
+ "𐳍": "",
+ "𐳎": "",
+ "𐳏": "",
+ "𐳐": "",
+ "𐳑": "",
+ "𐳒": "",
+ "𐳓": "",
+ "𐳔": "",
+ "𐳕": "",
+ "𐳖": "",
+ "𐳗": "",
+ "𐳘": "",
+ "𐳙": "",
+ "𐳚": "",
+ "𐳛": "",
+ "𐳜": "",
+ "𐳝": "",
+ "𐳞": "",
+ "𐳟": "",
+ "𐳠": "",
+ "𐳡": "",
+ "𐳢": "",
+ "𐳣": "",
+ "𐳤": "",
+ "𐳥": "",
+ "𐳦": "",
+ "𐳧": "",
+ "𐳨": "",
+ "𐳩": "",
+ "𐳪": "",
+ "𐳫": "",
+ "𐳬": "",
+ "𐳭": "",
+ "𐳮": "",
+ "𐳯": "",
+ "𐳰": "",
+ "𐳱": "",
+ "𐳲": "",
+ "𑣀": "",
+ "𑣁": "",
+ "𑣂": "",
+ "𑣃": "",
+ "𑣄": "",
+ "𑣅": "",
+ "𑣆": "",
+ "𑣇": "",
+ "𑣈": "",
+ "𑣉": "",
+ "𑣊": "",
+ "𑣋": "",
+ "𑣌": "",
+ "𑣍": "",
+ "𑣎": "",
+ "𑣏": "",
+ "𑣐": "",
+ "𑣑": "",
+ "𑣒": "",
+ "𑣓": "",
+ "𑣔": "",
+ "𑣕": "",
+ "𑣖": "",
+ "𑣗": "",
+ "𑣘": "",
+ "𑣙": "",
+ "𑣚": "",
+ "𑣛": "",
+ "𑣜": "",
+ "𑣝": "",
+ "𑣞": "",
+ "𑣟": "",
+ "𖹠": "",
+ "𖹡": "",
+ "𖹢": "",
+ "𖹣": "",
+ "𖹤": "",
+ "𖹥": "",
+ "𖹦": "",
+ "𖹧": "",
+ "𖹨": "",
+ "𖹩": "",
+ "𖹪": "",
+ "𖹫": "",
+ "𖹬": "",
+ "𖹭": "",
+ "𖹮": "",
+ "𖹯": "",
+ "𖹰": "",
+ "𖹱": "",
+ "𖹲": "",
+ "𖹳": "",
+ "𖹴": "",
+ "𖹵": "",
+ "𖹶": "",
+ "𖹷": "",
+ "𖹸": "",
+ "𖹹": "",
+ "𖹺": "",
+ "𖹻": "",
+ "𖹼": "",
+ "𖹽": "",
+ "𖹾": "",
+ "𖹿": "",
+ "𞤢": "",
+ "𞤣": "",
+ "𞤤": "",
+ "𞤥": "",
+ "𞤦": "",
+ "𞤧": "",
+ "𞤨": "",
+ "𞤩": "",
+ "𞤪": "",
+ "𞤫": "",
+ "𞤬": "",
+ "𞤭": "",
+ "𞤮": "",
+ "𞤯": "",
+ "𞤰": "",
+ "𞤱": "",
+ "𞤲": "",
+ "𞤳": "",
+ "𞤴": "",
+ "𞤵": "",
+ "𞤶": "",
+ "𞤷": "",
+ "𞤸": "",
+ "𞤹": "",
+ "𞤺": "",
+ "𞤻": "",
+ "𞤼": "",
+ "𞤽": "",
+ "𞤾": "",
+ "𞤿": "",
+ "𞥀": "",
+ "𞥁": "",
+ "𞥂": "",
+ "𞥃": ""
}
[],
'/rest',
new \EmptyBagOStuff(),
- new ResponseFactory(),
+ new ResponseFactory( [] ),
new MWBasicAuthorizer( $user, MediaWikiServices::getInstance()->getPermissionManager() ),
$objectFactory,
new Validator( $objectFactory, $request, $user )
[],
'/rest',
new EmptyBagOStuff(),
- new ResponseFactory(),
+ new ResponseFactory( [] ),
new StaticBasicAuthorizer(),
$objectFactory,
new Validator( $objectFactory, $request, new User )
[],
'/rest',
new EmptyBagOStuff(),
- new ResponseFactory(),
+ new ResponseFactory( [] ),
new StaticBasicAuthorizer(),
$objectFactory,
new Validator( $objectFactory, $request, new User )
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 {
];
}
+ 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' ) );
}
public function testCreateNoContent() {
- $rf = new ResponseFactory;
+ $rf = $this->createResponseFactory();
$response = $rf->createNoContent();
$this->assertSame( [], $response->getHeader( 'Content-Type' ) );
$this->assertSame( 0, $response->getBody()->getSize() );
}
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() );
/** @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();
}
public function testCreateFromExceptionUnlogged() {
- $rf = new ResponseFactory;
+ $rf = $this->createResponseFactory();
$response = $rf->createFromException( new HttpException( 'hello', 415 ) );
$this->assertSame( 415, $response->getStatusCode() );
$body = $response->getBody();
}
public function testCreateFromExceptionLogged() {
- $rf = new ResponseFactory;
+ $rf = $this->createResponseFactory();
$response = $rf->createFromException( new \Exception( "hello", 415 ) );
$this->assertSame( 500, $response->getStatusCode() );
$body = $response->getBody();
/** @dataProvider provideCreateFromReturnValue */
public function testCreateFromReturnValue( $input, $expected ) {
- $rf = new ResponseFactory;
+ $rf = $this->createResponseFactory();
$response = $rf->createFromReturnValue( $input );
$body = $response->getBody();
$body->rewind();
/** @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() );
+ }
}
[],
'/rest',
new \EmptyBagOStuff(),
- new ResponseFactory(),
+ new ResponseFactory( [] ),
new StaticBasicAuthorizer( $authError ),
$objectFactory,
new Validator( $objectFactory, $request, new User )
$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 );
* 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
/**
* 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
/**
* 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
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