From: jenkins-bot Date: Thu, 29 Aug 2019 05:25:40 +0000 (+0000) Subject: Merge "profiler: Centralise output responsibility from ProfilerOutputText to Profiler" X-Git-Tag: 1.34.0-rc.0~509 X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=commitdiff_plain;h=b4675c6125736c11b5a4c14c25a2bfaf41102264;hp=8a87ec277858c4e72e8f654627c5363ec979771b Merge "profiler: Centralise output responsibility from ProfilerOutputText to Profiler" --- diff --git a/Gruntfile.js b/Gruntfile.js index f3950f6f50..8115ea2aec 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -26,7 +26,7 @@ module.exports = function ( grunt ) { cache: true }, all: [ - '**/*.js{,on}', + '**/*.{js,json}', '!docs/**', '!node_modules/**', '!resources/lib/**', diff --git a/includes/Permissions/PermissionManager.php b/includes/Permissions/PermissionManager.php index 43d57a7748..f9ad3ebb93 100644 --- a/includes/Permissions/PermissionManager.php +++ b/includes/Permissions/PermissionManager.php @@ -1244,11 +1244,12 @@ class PermissionManager { */ public function getUserPermissions( UserIdentity $user ) { $user = User::newFromIdentity( $user ); - if ( !isset( $this->usersRights[ $user->getId() ] ) ) { - $this->usersRights[ $user->getId() ] = $this->getGroupPermissions( + $rightsCacheKey = $this->getRightsCacheKey( $user ); + if ( !isset( $this->usersRights[ $rightsCacheKey ] ) ) { + $this->usersRights[ $rightsCacheKey ] = $this->getGroupPermissions( $user->getEffectiveGroups() ); - Hooks::run( 'UserGetRights', [ $user, &$this->usersRights[ $user->getId() ] ] ); + Hooks::run( 'UserGetRights', [ $user, &$this->usersRights[ $rightsCacheKey ] ] ); // Deny any rights denied by the user's session, unless this // endpoint has no sessions. @@ -1256,17 +1257,17 @@ class PermissionManager { // FIXME: $user->getRequest().. need to be replaced with something else $allowedRights = $user->getRequest()->getSession()->getAllowedUserRights(); if ( $allowedRights !== null ) { - $this->usersRights[ $user->getId() ] = array_intersect( - $this->usersRights[ $user->getId() ], + $this->usersRights[ $rightsCacheKey ] = array_intersect( + $this->usersRights[ $rightsCacheKey ], $allowedRights ); } } - Hooks::run( 'UserGetRightsRemove', [ $user, &$this->usersRights[ $user->getId() ] ] ); + Hooks::run( 'UserGetRightsRemove', [ $user, &$this->usersRights[ $rightsCacheKey ] ] ); // Force reindexation of rights when a hook has unset one of them - $this->usersRights[ $user->getId() ] = array_values( - array_unique( $this->usersRights[ $user->getId() ] ) + $this->usersRights[ $rightsCacheKey ] = array_values( + array_unique( $this->usersRights[ $rightsCacheKey ] ) ); if ( @@ -1275,13 +1276,13 @@ class PermissionManager { $user->getBlock() ) { $anon = new User; - $this->usersRights[ $user->getId() ] = array_intersect( - $this->usersRights[ $user->getId() ], + $this->usersRights[ $rightsCacheKey ] = array_intersect( + $this->usersRights[ $rightsCacheKey ], $this->getUserPermissions( $anon ) ); } } - $rights = $this->usersRights[ $user->getId() ]; + $rights = $this->usersRights[ $rightsCacheKey ]; foreach ( $this->temporaryUserRights[ $user->getId() ] ?? [] as $overrides ) { $rights = array_values( array_unique( array_merge( $rights, $overrides ) ) ); } @@ -1298,14 +1299,24 @@ class PermissionManager { */ public function invalidateUsersRightsCache( $user = null ) { if ( $user !== null ) { - if ( isset( $this->usersRights[ $user->getId() ] ) ) { - unset( $this->usersRights[$user->getId()] ); + $rightsCacheKey = $this->getRightsCacheKey( $user ); + if ( isset( $this->usersRights[ $rightsCacheKey ] ) ) { + unset( $this->usersRights[ $rightsCacheKey ] ); } } else { $this->usersRights = null; } } + /** + * Gets a unique key for user rights cache. + * @param UserIdentity $user + * @return string + */ + private function getRightsCacheKey( UserIdentity $user ) { + return $user->isRegistered() ? "u:{$user->getId()}" : "anon:{$user->getName()}"; + } + /** * Check, if the given group has the given permission * @@ -1583,7 +1594,8 @@ class PermissionManager { if ( !defined( 'MW_PHPUNIT_TEST' ) ) { throw new Exception( __METHOD__ . ' can not be called outside of tests' ); } - $this->usersRights[ $user->getId() ] = is_array( $rights ) ? $rights : [ $rights ]; + $this->usersRights[ $this->getRightsCacheKey( $user ) ] = + is_array( $rights ) ? $rights : [ $rights ]; } } diff --git a/includes/filebackend/FileBackendGroup.php b/includes/filebackend/FileBackendGroup.php index 8b31f05c83..9e04d09632 100644 --- a/includes/filebackend/FileBackendGroup.php +++ b/includes/filebackend/FileBackendGroup.php @@ -150,6 +150,7 @@ class FileBackendGroup { $class = $config['class']; if ( $class === FileBackendMultiWrite::class ) { + // @todo How can we test this? What's the intended use-case? foreach ( $config['backends'] as $index => $beConfig ) { if ( isset( $beConfig['template'] ) ) { // Config is just a modified version of a registered backend's. diff --git a/includes/libs/filebackend/filejournal/FileJournal.php b/includes/libs/filebackend/filejournal/FileJournal.php index dc007a0cec..e51242375a 100644 --- a/includes/libs/filebackend/filejournal/FileJournal.php +++ b/includes/libs/filebackend/filejournal/FileJournal.php @@ -26,6 +26,7 @@ * @ingroup FileJournal */ +use Wikimedia\ObjectFactory; use Wikimedia\Timestamp\ConvertibleTimestamp; /** @@ -43,12 +44,12 @@ abstract class FileJournal { protected $ttlDays; /** - * Construct a new instance from configuration. + * Construct a new instance from configuration. Do not call this directly, use factory(). * * @param array $config Includes: * 'ttlDays' : days to keep log entries around (false means "forever") */ - protected function __construct( array $config ) { + public function __construct( array $config ) { $this->ttlDays = $config['ttlDays'] ?? false; } @@ -61,11 +62,10 @@ abstract class FileJournal { * @return FileJournal */ final public static function factory( array $config, $backend ) { - $class = $config['class']; - $jrn = new $class( $config ); - if ( !$jrn instanceof self ) { - throw new InvalidArgumentException( "$class is not an instance of " . __CLASS__ ); - } + $jrn = ObjectFactory::getObjectFromSpec( + $config, + [ 'specIsArg' => true, 'assertClass' => __CLASS__ ] + ); $jrn->backend = $backend; return $jrn; diff --git a/includes/specialpage/WantedQueryPage.php b/includes/specialpage/WantedQueryPage.php index 83ffe40a51..72fe57d08a 100644 --- a/includes/specialpage/WantedQueryPage.php +++ b/includes/specialpage/WantedQueryPage.php @@ -116,7 +116,7 @@ abstract class WantedQueryPage extends QueryPage { * @param object $result Result row * @return string */ - private function makeWlhLink( $title, $result ) { + protected function makeWlhLink( $title, $result ) { $wlh = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedText() ); $label = $this->msg( 'nlinks' )->numParams( $result->value )->text(); return $this->getLinkRenderer()->makeLink( $wlh, $label ); diff --git a/includes/specials/SpecialContributions.php b/includes/specials/SpecialContributions.php index f1843ead93..9d5f430b3c 100644 --- a/includes/specials/SpecialContributions.php +++ b/includes/specials/SpecialContributions.php @@ -203,8 +203,6 @@ class SpecialContributions extends IncludableSpecialPage { } $pager = new ContribsPager( $this->getContext(), [ 'target' => $target, - // Temporary, until newbie feature is fully removed from ContribsPager - 'contribs' => 'user', 'namespace' => $this->opts['namespace'], 'tagfilter' => $this->opts['tagfilter'], 'start' => $this->opts['start'], diff --git a/includes/specials/SpecialDeletedContributions.php b/includes/specials/SpecialDeletedContributions.php index 40d89625eb..902bfd7853 100644 --- a/includes/specials/SpecialDeletedContributions.php +++ b/includes/specials/SpecialDeletedContributions.php @@ -113,17 +113,15 @@ class DeletedContributionsPage extends SpecialPage { # If there were contributions, and it was a valid user or IP, show # the appropriate "footer" message - WHOIS tools, etc. - if ( $target != 'newbies' ) { - $message = IP::isIPAddress( $target ) ? - 'sp-contributions-footer-anon' : - 'sp-contributions-footer'; - - if ( !$this->msg( $message )->isDisabled() ) { - $out->wrapWikiMsg( - "", - [ $message, $target ] - ); - } + $message = IP::isIPAddress( $target ) ? + 'sp-contributions-footer-anon' : + 'sp-contributions-footer'; + + if ( !$this->msg( $message )->isDisabled() ) { + $out->wrapWikiMsg( + "", + [ $message, $target ] + ); } } diff --git a/includes/specials/SpecialNewimages.php b/includes/specials/SpecialNewimages.php index e0834d5bd5..ecbbfd5fc9 100644 --- a/includes/specials/SpecialNewimages.php +++ b/includes/specials/SpecialNewimages.php @@ -131,7 +131,7 @@ class SpecialNewFiles extends IncludableSpecialPage { ], 'user' => [ - 'type' => 'text', + 'class' => 'HTMLUserTextField', 'label-message' => 'newimages-user', 'name' => 'user', ], diff --git a/includes/specials/pagers/ContribsPager.php b/includes/specials/pagers/ContribsPager.php index 1b0c59ab25..d62951cb7e 100644 --- a/includes/specials/pagers/ContribsPager.php +++ b/includes/specials/pagers/ContribsPager.php @@ -41,12 +41,6 @@ class ContribsPager extends RangeChronologicalPager { */ private $target; - /** - * @var string Set to "newbie" to list contributions from the most recent 1% registered users. - * $this->target is ignored then. Defaults to "users". - */ - private $contribs; - /** * @var string|int A single namespace number, or an empty string for all namespaces */ @@ -104,11 +98,10 @@ class ContribsPager extends RangeChronologicalPager { private $templateParser; public function __construct( IContextSource $context, array $options ) { - // Set ->target and ->contribs before calling parent::__construct() so + // Set ->target before calling parent::__construct() so // parent can call $this->getIndexField() and get the right result. Set // the rest too just to keep things simple. $this->target = $options['target'] ?? ''; - $this->contribs = $options['contribs'] ?? 'users'; $this->namespace = $options['namespace'] ?? ''; $this->tagFilter = $options['tagfilter'] ?? false; $this->nsInvert = $options['nsInvert'] ?? false; @@ -249,10 +242,6 @@ class ContribsPager extends RangeChronologicalPager { * @return string */ private function getTargetTable() { - if ( $this->contribs == 'newbie' ) { - return 'revision'; - } - $user = User::newFromName( $this->target, false ); $ipRangeConds = $user->isAnon() ? $this->getIpRangeConds( $this->mDb, $this->target ) : null; if ( $ipRangeConds ) { @@ -279,53 +268,25 @@ class ContribsPager extends RangeChronologicalPager { ]; // WARNING: Keep this in sync with getTargetTable()! - - if ( $this->contribs == 'newbie' ) { - $max = $this->mDb->selectField( 'user', 'max(user_id)', '', __METHOD__ ); - $queryInfo['conds'][] = $revQuery['fields']['rev_user'] . ' >' . (int)( $max - $max / 100 ); - # ignore local groups with the bot right - # @todo FIXME: Global groups may have 'bot' rights - $groupsWithBotPermission = MediaWikiServices::getInstance() - ->getPermissionManager() - ->getGroupsWithPermission( 'bot' ); - if ( count( $groupsWithBotPermission ) ) { - $queryInfo['tables'][] = 'user_groups'; - $queryInfo['conds'][] = 'ug_group IS NULL'; - $queryInfo['join_conds']['user_groups'] = [ - 'LEFT JOIN', [ - 'ug_user = ' . $revQuery['fields']['rev_user'], - 'ug_group' => $groupsWithBotPermission, - 'ug_expiry IS NULL OR ug_expiry >= ' . - $this->mDb->addQuotes( $this->mDb->timestamp() ) - ] - ]; - } - // (T140537) Disallow looking too far in the past for 'newbies' queries. If the user requested - // a timestamp offset far in the past such that there are no edits by users with user_ids in - // the range, we would end up scanning all revisions from that offset until start of time. - $queryInfo['conds'][] = 'rev_timestamp > ' . - $this->mDb->addQuotes( $this->mDb->timestamp( wfTimestamp() - 30 * 24 * 60 * 60 ) ); + $user = User::newFromName( $this->target, false ); + $ipRangeConds = $user->isAnon() ? $this->getIpRangeConds( $this->mDb, $this->target ) : null; + if ( $ipRangeConds ) { + $queryInfo['tables'][] = 'ip_changes'; + $queryInfo['join_conds']['ip_changes'] = [ + 'LEFT JOIN', [ 'ipc_rev_id = rev_id' ] + ]; + $queryInfo['conds'][] = $ipRangeConds; } else { - $user = User::newFromName( $this->target, false ); - $ipRangeConds = $user->isAnon() ? $this->getIpRangeConds( $this->mDb, $this->target ) : null; - if ( $ipRangeConds ) { - $queryInfo['tables'][] = 'ip_changes'; - $queryInfo['join_conds']['ip_changes'] = [ - 'LEFT JOIN', [ 'ipc_rev_id = rev_id' ] - ]; - $queryInfo['conds'][] = $ipRangeConds; + // tables and joins are already handled by Revision::getQueryInfo() + $conds = ActorMigration::newMigration()->getWhere( $this->mDb, 'rev_user', $user ); + $queryInfo['conds'][] = $conds['conds']; + // Force the appropriate index to avoid bad query plans (T189026) + if ( isset( $conds['orconds']['actor'] ) ) { + // @todo: This will need changing when revision_actor_temp goes away + $queryInfo['options']['USE INDEX']['temp_rev_user'] = 'actor_timestamp'; } else { - // tables and joins are already handled by Revision::getQueryInfo() - $conds = ActorMigration::newMigration()->getWhere( $this->mDb, 'rev_user', $user ); - $queryInfo['conds'][] = $conds['conds']; - // Force the appropriate index to avoid bad query plans (T189026) - if ( isset( $conds['orconds']['actor'] ) ) { - // @todo: This will need changing when revision_actor_temp goes away - $queryInfo['options']['USE INDEX']['temp_rev_user'] = 'actor_timestamp'; - } else { - $queryInfo['options']['USE INDEX']['revision'] = - isset( $conds['orconds']['userid'] ) ? 'user_timestamp' : 'usertext_timestamp'; - } + $queryInfo['options']['USE INDEX']['revision'] = + isset( $conds['orconds']['userid'] ) ? 'user_timestamp' : 'usertext_timestamp'; } } @@ -478,13 +439,6 @@ class ContribsPager extends RangeChronologicalPager { return $this->tagFilter; } - /** - * @return string - */ - public function getContribs() { - return $this->contribs; - } - /** * @return string */ @@ -544,10 +498,7 @@ class ContribsPager extends RangeChronologicalPager { } if ( isset( $row->rev_id ) ) { $this->mParentLens[$row->rev_id] = $row->rev_len; - if ( $this->contribs === 'newbie' ) { // multiple users - $batch->add( NS_USER, $row->user_name ); - $batch->add( NS_USER_TALK, $row->user_name ); - } elseif ( $isIpRange ) { + if ( $isIpRange ) { // If this is an IP range, batch the IP's talk page $batch->add( NS_USER_TALK, $row->rev_user_text ); } @@ -702,12 +653,9 @@ class ContribsPager extends RangeChronologicalPager { $comment = $lang->getDirMark() . Linker::revComment( $rev, false, true, false ); $d = ChangesList::revDateLink( $rev, $user, $lang, $page ); - # Show user names for /newbies as there may be different users. - # Note that only unprivileged users have rows with hidden user names excluded. # When querying for an IP range, we want to always show user and user talk links. $userlink = ''; - if ( ( $this->contribs == 'newbie' && !$rev->isDeleted( RevisionRecord::DELETED_USER ) ) - || $this->isQueryableRange( $this->target ) ) { + if ( $this->isQueryableRange( $this->target ) ) { $userlink = ' ' . $lang->getDirMark() . Linker::userLink( $rev->getUser(), $rev->getUserText() ); diff --git a/includes/specials/pagers/NewFilesPager.php b/includes/specials/pagers/NewFilesPager.php index 57db8b3f5f..2cb2b4aaad 100644 --- a/includes/specials/pagers/NewFilesPager.php +++ b/includes/specials/pagers/NewFilesPager.php @@ -72,20 +72,6 @@ class NewFilesPager extends RangeChronologicalPager { ->getWhere( wfGetDB( DB_REPLICA ), 'img_user', User::newFromName( $user, false ) )['conds']; } - if ( $opts->getValue( 'newbies' ) ) { - // newbie = most recent 1% of users - $dbr = wfGetDB( DB_REPLICA ); - $max = $dbr->selectField( 'user', 'max(user_id)', '', __METHOD__ ); - $conds[] = $imgQuery['fields']['img_user'] . ' >' . (int)( $max - $max / 100 ); - - // there's no point in looking for new user activity in a far past; - // beyond a certain point, we'd just end up scanning the rest of the - // table even though the users we're looking for didn't yet exist... - // see T140537, (for ContribsPages, but similar to this) - $conds[] = 'img_timestamp > ' . - $dbr->addQuotes( $dbr->timestamp( wfTimestamp() - 30 * 24 * 60 * 60 ) ); - } - if ( !$opts->getValue( 'showbots' ) ) { $groupsWithBotPermission = MediaWikiServices::getInstance() ->getPermissionManager() diff --git a/includes/upload/UploadFromChunks.php b/includes/upload/UploadFromChunks.php index 8c6b2f9c05..c28890baa1 100644 --- a/includes/upload/UploadFromChunks.php +++ b/includes/upload/UploadFromChunks.php @@ -161,7 +161,7 @@ class UploadFromChunks extends UploadFromFile { $ext = FileBackend::extensionFromPath( $this->mVirtualTempPath ); // Get a 0-byte temp file to perform the concatenation at $tmpFile = MediaWikiServices::getInstance()->getTempFSFileFactory() - ->getTempFSFile( 'chunkedupload_', $ext ); + ->newTempFSFile( 'chunkedupload_', $ext ); $tmpPath = false; // fail in concatenate() if ( $tmpFile ) { // keep alive with $this diff --git a/languages/i18n/en.json b/languages/i18n/en.json index 3220f7a318..106d6a7dc6 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -2360,7 +2360,6 @@ "wlheader-enotif": "Email notification is enabled.", "wlheader-showupdated": "Pages that have been changed since you last visited them are shown in bold.", "wlnote": "Below {{PLURAL:$1|is the last change|are the last $1 changes}} in the last {{PLURAL:$2|hour|$2 hours}}, as of $3, $4.", - "wlshowlast": "Show last $1 hours $2 days", "watchlist-hide": "Hide", "watchlist-submit": "Show", "wlshowtime": "Period of time to display:", @@ -3403,8 +3402,6 @@ "img-lang-default": "(default language)", "img-lang-info": "Render this image in $1. $2", "img-lang-go": "Go", - "ascending_abbrev": "asc", - "descending_abbrev": "desc", "table_pager_next": "Next page", "table_pager_prev": "Previous page", "table_pager_first": "First page", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index aa33ee0aa9..5fdcb7dc37 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -2569,7 +2569,6 @@ "wlheader-enotif": "Message at the top of [[Special:Watchlist]], after {{msg-mw|watchlist-details}}. Has to be a full sentence.\n\nSee also:\n* {{msg-mw|Watchlist-options|fieldset}}\n* {{msg-mw|enotif reset|Submit button text}}", "wlheader-showupdated": "Message at the top of [[Special:Watchlist]], after {{msg-mw|watchlist-details}}. Has to be a full sentence.", "wlnote": "Used on [[Special:Watchlist]] when a maximum number of hours or days is specified.\n\nParameters:\n* $1 - the number of changes shown\n* $2 - the number of hours for which the changes are shown\n* $3 - a date alone\n* $4 - a time alone", - "wlshowlast": "Appears on [[Special:Watchlist]]. Parameters:\n* $1 - a choice of different numbers of hours (\"1 | 2 | 6 | 12\")\n* $2 - a choice of different numbers of days (\"1 | 3 | 7\" and the maximum number of days available)\nClicking on your choice changes the list of changes you see (without changing the default in my preferences).", "watchlist-hide": "Appears on [[Special:Watchlist]]. It is the first word on a new line with checkboxes to hide/unhide options\n{{Identical|Hide}}", "watchlist-submit": "Label on the submit button in [[Special:Watchlist]]\n{{Identical|Show}}", "wlshowtime": "Appears on [[Special:Watchlist]]. Label of a drop-down list used to specify the period of time to display in the watchlist. This period can be {{msg-mw|days}} or {{msg-mw|hours}}.", @@ -3612,8 +3611,6 @@ "img-lang-default": "An option in the drop down of a translatable file. For example see [[:File:Gerrit patchset 25838 test.svg]].\n\nUsed when it cannot be determined what the default fallback language is.\n\nHowever it should be noted that most of the time, the content displayed for this option would be in English.\n{{Identical|Default language}}", "img-lang-info": "Label for drop down box. Appears underneath the image on the image description page. See [[:File:Gerrit patchset 25838 test.svg]] for an example.\n\nParameters:\n* $1 - a drop down box with language options, uses the following messages:\n** {{msg-mw|Img-lang-default}}\n** {{msg-mw|Img-lang-opt}}. e.g. \"English (en)\", \"日本語 (ja)\"\n* $2 - a submit button, which uses the text from {{msg-mw|Img-lang-go}}", "img-lang-go": "Go button for the language select for translatable files. See [[:File:Gerrit patchset 25838 test.svg]] for an example.\n\nSee also:\n* {{msg-mw|img-lang-info}}\n{{Identical|Go}}", - "ascending_abbrev": "Abbreviation of ascending order.\nSee also:\n* {{msg-mw|Ascending abbrev}}\n* {{msg-mw|Descending abbrev}}", - "descending_abbrev": "Abbreviation of descending order.\nSee also:\n* {{msg-mw|Ascending abbrev}}\n* {{msg-mw|Descending abbrev}}", "table_pager_next": "Used as image button text of pager. See [[Support|example]] (the bottom of the page).\n{{Identical|Next page}}", "table_pager_prev": "Used as image button text of pager. See [[Support|example]] (the bottom of the page).\n{{Identical|Previous page}}", "table_pager_first": "Used as image button text of pager. See [[Support|example]] (the bottom of the page).\n{{Identical|First page}}", diff --git a/resources/Resources.php b/resources/Resources.php index d33e3dee2c..8234e8970c 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -1042,6 +1042,12 @@ return [ 'mediawiki.pager.tablePager' => [ 'styles' => 'resources/src/mediawiki.pager.tablePager/TablePager.less', ], + 'mediawiki.pulsatingdot' => [ + 'styles' => [ + 'resources/src/mediawiki.pulsatingdot/mediawiki.pulsatingdot.less', + ], + 'targets' => [ 'desktop', 'mobile' ], + ], 'mediawiki.searchSuggest' => [ 'targets' => [ 'desktop', 'mobile' ], 'scripts' => 'resources/src/mediawiki.searchSuggest/searchSuggest.js', @@ -1370,7 +1376,7 @@ return [ 'oojs-ui-core', ], 'messages' => [ - // Keep the uses message keys in sync with EditPage#setHeaders + // Keep these message keys in sync with EditPage#setHeaders 'creating', 'editconflict', 'editing', diff --git a/resources/src/mediawiki.pulsatingdot/mediawiki.pulsatingdot.less b/resources/src/mediawiki.pulsatingdot/mediawiki.pulsatingdot.less new file mode 100644 index 0000000000..00a5608230 --- /dev/null +++ b/resources/src/mediawiki.pulsatingdot/mediawiki.pulsatingdot.less @@ -0,0 +1,70 @@ +.mw-pulsating-dot { + &:before, + &:after { + content: ''; + display: block; + position: absolute; + border-radius: 50%; + background-color: #36c; + } + + &:before { + width: 36px; + height: 36px; + top: -18px; + left: -18px; + opacity: 0; + -webkit-animation: mw-pulsating-dot-pulse 3s ease-out; + -moz-animation: mw-pulsating-dot-pulse 3s ease-out; + animation: mw-pulsating-dot-pulse 3s ease-out; + -webkit-animation-iteration-count: infinite; + -moz-animation-iteration-count: infinite; + animation-iteration-count: infinite; + } + + &:after { + width: 12px; + height: 12px; + top: -6px; + left: -6px; + } +} + +.mw-pulsating-dot-pulse-frames() { + 0% { + transform: scale( 0 ); + opacity: 0; + } + + 25% { + transform: scale( 0 ); + opacity: 0.1; + } + + 50% { + transform: scale( 0.1 ); + opacity: 0.3; + } + + 75% { + transform: scale( 0.5 ); + opacity: 0.5; + } + + 100% { + transform: scale( 1 ); + opacity: 0; + } +} + +@-webkit-keyframes mw-pulsating-dot-pulse { + .mw-pulsating-dot-pulse-frames; +} + +@-moz-keyframes mw-pulsating-dot-pulse { + .mw-pulsating-dot-pulse-frames; +} + +@keyframes mw-pulsating-dot-pulse { + .mw-pulsating-dot-pulse-frames; +} diff --git a/resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.js b/resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.js index 5ca39d5d45..dfc41be8eb 100644 --- a/resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.js +++ b/resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.js @@ -110,7 +110,9 @@ // We have no way to display a translated placeholder for custom formats placeholderDateFormat = ''; } else { - // Messages: mw-widgets-dateinput-placeholder-day, mw-widgets-dateinput-placeholder-month + // The following messages are used here: + // * mw-widgets-dateinput-placeholder-day + // * mw-widgets-dateinput-placeholder-month placeholderDateFormat = mw.msg( 'mw-widgets-dateinput-placeholder-' + config.precision ); } diff --git a/resources/src/startup/mediawiki.js b/resources/src/startup/mediawiki.js index a3249de6f1..a4ee488885 100644 --- a/resources/src/startup/mediawiki.js +++ b/resources/src/startup/mediawiki.js @@ -2096,9 +2096,8 @@ // Whether the store is in use on this page. enabled: null, - // Modules whose string representation exceeds 100 kB are - // ineligible for storage. See bug T66721. - MODULE_SIZE_MAX: 100 * 1000, + // Modules whose serialised form exceeds 100 kB won't be stored (T66721). + MODULE_SIZE_MAX: 1e5, // The contents of the store, mapping '[name]@[version]' keys // to module implementations. @@ -2117,7 +2116,13 @@ * @return {Object} Module store contents. */ toJSON: function () { - return { items: mw.loader.store.items, vary: mw.loader.store.vary }; + return { + items: mw.loader.store.items, + vary: mw.loader.store.vary, + // Store with 1e7 ms accuracy (1e4 seconds, or ~ 2.7 hours), + // which is enough for the purpose of expiring after ~ 30 days. + asOf: Math.ceil( Date.now() / 1e7 ) + }; }, /** @@ -2175,7 +2180,14 @@ this.enabled = true; // If null, JSON.parse() will cast to string and re-parse, still null. data = JSON.parse( raw ); - if ( data && typeof data.items === 'object' && data.vary === this.vary ) { + if ( data && + typeof data.items === 'object' && + data.vary === this.vary && + // Only use if it's been less than 30 days since the data was written + // 30 days = 2,592,000 s = 2,592,000,000 ms = ± 259e7 ms + Date.now() < ( data.asOf * 1e7 ) + 259e7 + ) { + // The data is not corrupt, matches our vary context, and has not expired. this.items = data.items; return; } diff --git a/tests/common/TestsAutoLoader.php b/tests/common/TestsAutoLoader.php index 7c8df1a6f5..9f1d67be16 100644 --- a/tests/common/TestsAutoLoader.php +++ b/tests/common/TestsAutoLoader.php @@ -221,6 +221,9 @@ $wgAutoloadClasses += [ # tests/phpunit/unit/includes 'BadFileLookupTest' => "$testDir/phpunit/unit/includes/BadFileLookupTest.php", + # tests/phpunit/unit/includes/filebackend + 'FileBackendGroupTestTrait' => "$testDir/phpunit/unit/includes/filebackend/FileBackendGroupTestTrait.php", + # tests/phpunit/unit/includes/libs/filebackend/fsfile 'TempFSFileTestTrait' => "$testDir/phpunit/unit/includes/libs/filebackend/fsfile/TempFSFileTestTrait.php", diff --git a/tests/parser/parserTests.txt b/tests/parser/parserTests.txt index 0fa91d4afc..d563235cc0 100644 --- a/tests/parser/parserTests.txt +++ b/tests/parser/parserTests.txt @@ -3182,7 +3182,7 @@ Parsoid: Pipes in external links in template parameter

link

!! html/parsoid -

link

+

link

!! end !! test @@ -3193,7 +3193,7 @@ Parsoid: pipe in transclusion parameter

http://foo.com/a%7Cb

!! html/parsoid -

http://foo.com/a%7Cb

+

http://foo.com/a%7Cb

!! end !! test @@ -3220,7 +3220,7 @@ Parsoid: Pipe in template with nested template in external link target in templa

bar

!! html/parsoid -

bar

+

bar

!! end !! test @@ -4049,7 +4049,7 @@ Definition list with news link containing colon
news:alt.wikipedia.rox
This isn't even a real newsgroup!
!! html/parsoid -
news:alt.wikipedia.rox
This isn't even a real newsgroup!
+
news:alt.wikipedia.rox
This isn't even a real newsgroup!
!! end !! test @@ -4670,7 +4670,7 @@ Definition Lists: Mixed Lists: Test 13 !! end # FIXME: Maybe get rid of this test? -# From whitelist: +# From old whitelist description: # * The test is wrong, there are two colons where there should be :; # * The PHP parser is wrong to close the
after the
containing the !! html/parsoid -

http://[2404:130:0:1000::187:2]/index.php

+

http://[2404:130:0:1000::187:2]/index.php

Examples from RFC 2373, section 2.2:

- +

Examples from RFC 2732, section 2:

- + !! end !! test @@ -5936,24 +5963,24 @@ Examples from RFC 2732, section 2:
  • 6
  • 7
  • !! html/parsoid -

    test

    +

    test

    Examples from RFC 2373, section 2.2:

    - +

    Examples from RFC 2732, section 2:

    - + !! end !! test @@ -5999,7 +6026,7 @@ Non-extlinks in brackets [fool's] errand [fool's errand] [url=foo] -[url=http://example.com] +[url=http://example.com] [http:// bare protocols don't count]

    !! end @@ -6011,7 +6038,7 @@ Percent encoding in external links

    Search

    !! html/parsoid -

    Search

    +

    Search

    !! end !! test @@ -6022,7 +6049,7 @@ http://example.com

    http://example.com

    !! html/parsoid -

    http://example.com

    +

    http://example.com

    !! end !! test @@ -6054,14 +6081,14 @@ http://example.com/a)b

    foo

    !! html/parsoid -

    http://example.com)

    -

    http://example.com/test)

    -

    http://example.com/(test)

    -

    http://example.com/((test)

    -

    (http://example.com/(test))

    -

    (http://example.com/(test)))))

    -

    http://example.com/a)b

    -

    foo

    +

    http://example.com)

    +

    http://example.com/test)

    +

    http://example.com/(test)

    +

    http://example.com/((test)

    +

    (http://example.com/(test))

    +

    (http://example.com/(test)))))

    +

    http://example.com/a)b

    +

    foo

    !! end !! test @@ -6075,9 +6102,9 @@ Parenthesis in external links, w/ transclusion or comment

    (http://example.com)

    !! html/parsoid -

    (http://example.com/hi)

    +

    (http://example.com/hi)

    -

    (http://example.com)

    +

    (http://example.com)

    !! end !! test @@ -6654,6 +6681,8 @@ Allow +/- in 2nd and later cells in a row, in 1st cell when td-attrs are present !!end +# Differences between Parsoid and PHP re: trailing whitespace in a +# table cell. !! test Table rowspan !! wikitext @@ -6665,7 +6694,7 @@ Table rowspan |Cell 1, row 2 |Cell 3, row 2 |} -!! html +!! html/php
    Cell 1, row 1 @@ -6679,6 +6708,15 @@ Table rowspan Cell 3, row 2
    +!! html/parsoid + + + + + + + +
    Cell 1, row 1Cell 2, row 1 (and 2)Cell 3, row 1
    Cell 1, row 2Cell 3, row 2
    !! end !! test @@ -6771,7 +6809,7 @@ parsoid=wt2html,html2html !! html/parsoid -
    [ftp://%7Cx]" onmouseover="alert(document.cookie)">test
    +[ftp://%7Cx]" onmouseover="alert(document.cookie)">test !! end !! test @@ -7695,13 +7733,17 @@ Broken link

    !! end +# The PHP parser strips the hash fragment for non-existent pages, but +# Parsoid does not. (T227693) !! test Broken link with fragment !! wikitext [[Zigzagzogzagzig#zug]] -!! html +!! html/php

    Zigzagzogzagzig#zug

    +!! html/parsoid +

    Zigzagzogzagzig#zug

    !! end !! test @@ -7713,13 +7755,16 @@ Special page link with fragment

    !! end +# Parsoid does not strip fragment from red links: T227693 !! test Nonexistent special page link with fragment !! wikitext [[Special:ThisNameWillHopefullyNeverBeUsed#anchor]] -!! html +!! html/php

    Special:ThisNameWillHopefullyNeverBeUsed#anchor

    +!! html/parsoid +

    Special:ThisNameWillHopefullyNeverBeUsed#anchor

    !! end !! test @@ -8170,7 +8215,7 @@ Plain link to URL

    [[1]]

    !! html/parsoid -

    []

    +

    []

    !! end !! test @@ -8199,7 +8244,7 @@ Plain link to protocol-relative URL

    [[1]]

    !! html/parsoid -

    []

    +

    []

    !! end !! test @@ -8242,7 +8287,7 @@ Piped link to URL: [[http://www.example.com|an example URL]]

    Piped link to URL: [example URL]

    !! html/parsoid -

    Piped link to URL: [example URL]

    +

    Piped link to URL: [example URL]

    !! end !! test @@ -8264,13 +8309,13 @@ parsoid=wt2html

    [http://www.example.com

    !! html/parsoid -

    [http://www.example.com

    +

    [http://www.example.com

    -

    [|123]

    +

    [|123]

    -

    {{echo|[|123}}

    +

    {{echo|[|123}}

    -

    [http://www.example.com

    +

    [http://www.example.com

    !! end !! test @@ -8867,8 +8912,8 @@ Interwiki links that cannot be represented in wiki syntax

    meatball:ok ok with fragment ok ending with ? mark -has query -is just fragment

    +has query +is just fragment

    !! end !! test @@ -11445,7 +11490,7 @@ X[https://tools.ietf.org/html/rfc1234 foo]

    !! html/parsoid

    Xfoo

    -

    Xfoo

    +

    Xfoo

    !! end !! test @@ -11505,14 +11550,44 @@ Template with invalid target containing wikilink

    {{Main Page}}

    !! end +# The html2html output of this test is currently failing +# because the html2wt output is broken; see +# https://phabricator.wikimedia.org/T220018#5123777 for a discussion. +# Not (yet) including html2wt as a test mode because there are +# a couple of different correct ways this could be 'ed. !! test Template with just whitespace in it, T70421 !! wikitext {{echo|{{ }}}} +!! options +parsoid=wt2html,html2html +!! html/php+tidy +

    {{ }} +

    !! html/parsoid

    {{ }}

    !! end +# This is currently the wikitext output of html2wt on the above test +# case; note that it is broken! Adding a around the closing +# brace changes how the open braces associate, breaking the outer +# {{echo}} template invocation. *However* this "broken" wikitext +# exposed a useful tokenizer bug (T221384) in how the broken_template +# rule was being backtracked into, so it's a useful test case even +# if/when the above test case gets its html2wt output fixed. +!! test +Template with just whitespace (bad template brace matching) +!! options +parsoid=wt2html +!! wikitext +{{echo|{{ }}}} +!! html/php+tidy +

    {{echo|{{ }}}} +

    +!! html/parsoid +

    {{echo|{{ }}}}

    +!! end + !! article Template:test !! text @@ -11893,9 +11968,11 @@ Template:loop2 Template infinite loop !! wikitext {{loop1}} -!! html +!! html/php

    Template loop detected: Template:Loop1

    +!! html/parsoid +

    Template loop detected: Template:Loop1

    !! end !! test @@ -12034,7 +12111,7 @@ Templates with intersecting and overlapping ranges hi !! html/parsoid -

    ha

    +

    ha

    ho

    @@ -12115,9 +12192,11 @@ Template:Includes2 being included !! wikitext {{Includes2}} -!! html +!! html/php+tidy

    Foo

    +!! html/parsoid +

    Foo

    !! end @@ -12131,9 +12210,11 @@ Template:Includes3 and being included !! wikitext {{Includes3}} -!! html +!! html/php+tidy

    Foo

    +!! html/parsoid +

    Foo

    !! end # FIXME: Parsoid's markup for this is quite ugly. @@ -12152,7 +12233,20 @@ Foozarbar Un-closed !! wikitext -!! html +!! html/php+tidy +!! html/parsoid + +!! end + +!! test +Empty +!! wikitext +Hello! +!! html/php+tidy +

    Hello! +

    +!! html/parsoid +

    Hello!

    !! end !! test @@ -12273,6 +12367,7 @@ Un-closed ## will normalize the include directives to serialize on their own line. ## Selser will take care of preserving formatting in scenarios where they ## intermingled with other wikitext. +## This test also triggered T223411 during Parsoid-PHP porting. !! test Includes and comments at SOL !! options @@ -12813,9 +12908,9 @@ parsoid=wt2html
  • {{echo|Breaks template, however}}
  • !! html/parsoid !! end @@ -13877,7 +13972,7 @@ language=zh

    hi[1] hi[2] hi[3]

    -
    1. ↑ hi
    2. ↑
    3. ↑
    +
    1. ↑ hi
    2. ↑
    3. ↑
    !! end ### @@ -15867,7 +15962,7 @@ thumbsize=220 !! html/php !! html/parsoid -
    http://example.com
    +
    http://example.com
    !! end !! test @@ -15880,7 +15975,7 @@ parsoid=wt2html,wt2wt,html2html !! html/php !! html/parsoid -
    Alteration
    http://example.com
    +
    Alteration
    http://example.com
    !! end !! test @@ -15967,7 +16062,7 @@ T3887: A mailto link with a thumbnail !! html/php !! html/parsoid -
    Please mailto:nobody@example.com
    +
    Please mailto:nobody@example.com
    !! end # Pending resolution to T2368 @@ -16150,7 +16245,7 @@ T5090: External links other than http: in image captions !! html/php
    This caption has irc and Secure ext links in it.
    !! html/parsoid -
    This caption has irc and Secure ext links in it.
    +
    This caption has irc and Secure ext links in it.
    !! end !! test @@ -16650,7 +16745,7 @@ Render invalid page names as plain text (T53090) [[.]] [[..]] [[foo././bar]] -[[fooxyz]]

    +[[fooxyz]]

    [[./../foo|bar]] [[foo/.|bar]] @@ -16748,6 +16843,18 @@ cat=MediaWiki_User's_Guide sort=MediaWiki User's Guide !! end +!! test +Category with template-generated sort key +!! options +cat +!! wikitext +[[Category:MediaWiki User's Guide|MediaWiki {{echo|Foo}} Guide]] +!! html/php +cat=MediaWiki_User's_Guide sort=MediaWiki Foo Guide +!! html/parsoid + +!! end + !! test Category with empty sort key !! options @@ -17649,7 +17756,7 @@ http://example.com [[File:Foobar.jpg]]

    http://example.com Foobar.jpg

    !! html/parsoid -

    http://example.com

    +

    http://example.com

    !!end # Parsoid doesn't wt2wt this cleanly because it adds s. @@ -17952,7 +18059,7 @@ http://example.com[[File:Foobar.jpg]]

    http://example.comFoobar.jpg

    !! html/parsoid -

    http://example.com

    +

    http://example.com

    !!end !! test @@ -18192,6 +18299,17 @@ I always thought &xacute; was a cute letter.

    !! end +!! test +Text with HTML5 semicolon-less entity (should not decode) +!! wikitext +&amp; +!! html/php+tidy +

    &ampamp; +

    +!! html/parsoid +

    &ampamp;

    +!! end + !! test HTML5 tags !! wikitext @@ -19765,11 +19883,11 @@ mailto:inline@mail.tld

    mailto:inline@mail.tld

    !! html/parsoid -

    -

    ftp://inlineftp

    -

    With target

    -

    -

    mailto:inline@mail.tld

    +

    +

    ftp://inlineftp

    +

    With target

    +

    +

    mailto:inline@mail.tld

    !! end @@ -19817,7 +19935,7 @@ http://

    onmouseover= -

    http://__TOC__

    +

    http://__TOC__

    !! end !! test @@ -19884,12 +20002,31 @@ Fuzz testing: Parser22 http://===r:::https://b {| -!! html +!! html/php

    http://===r:::https://b

    +!! html/parsoid +

    http://===r:::https://b

    + +
    +!! end + +# The above 'Parser24' fuzz test exposed a tokenizer bug (T221384); +# this is a minimized version of the above test to catch regressions. +!! test +Fuzz testing: Parser24 (minimized) +!! options +parsoid=wt2html +!! wikitext +{{}} +!! html/php+tidy +

    {{}} +

    +!! html/parsoid +

    {{}}

    !! end ## Remex doesn't account for fostered content. @@ -19973,7 +20110,7 @@ http://example.com junk

    http://example.com junk

    !! html/parsoid -

    http://example.com junk

    +

    http://example.com junk

    !! end !!test @@ -19984,7 +20121,7 @@ http://example.comjunk

    http://example.comjunk

    !! html/parsoid -

    http://example.comjunk

    +

    http://example.comjunk

    !! end !! test @@ -19996,7 +20133,7 @@ http://example.com
    junk
    !! html/php+tidy

    http://example.com

    junk
    !! html/parsoid -

    http://example.com

    junk
    +

    http://example.com

    junk
    !! end !! test @@ -20020,7 +20157,7 @@ parsoid=wt2html
    
     !! html/parsoid
     
    
    +" typeof="mw:Extension/pre" about="#mwt2" data-mw='{"name":"pre","attrs":{"dir":""},"body":{"extsrc":""}}'>
     !! end
     
     !! test
    @@ -21049,7 +21186,7 @@ Handling of 
     in URLs
     !! html/php
     
     !! html/parsoid
    -
    +
     !! end
     
     !! test
    @@ -21059,7 +21196,7 @@ Handling of %0A in URLs
     !! html/php
     
     !! html/parsoid
    -
    +
     !! end
     
     # The PHP parser strips the empty tags out for giggles; parsoid doesn't.
    @@ -21258,7 +21395,7 @@ image4    |300px| centre
     
  • -
  • +
  • !! end @@ -21799,6 +21936,30 @@ File:Foobar.jpg !! end +!! test +Gallery in nolines mode +!! wikitext + +File:Foobar.jpg|foo + +!! html/php + +!! html/parsoid + +!! end + !! test Gallery in slideshow mode !! wikitext @@ -21835,7 +21996,55 @@ File:Foobar.jpg !! html/parsoid +!! end + +!! test +Gallery in packed-overlay mode +!! wikitext + +File:Foobar.jpg|foo + +!! html/php + +!! html/parsoid + +!! end + +!! test +Gallery in packed-hover mode +!! wikitext + +File:Foobar.jpg|foo + +!! html/php + +!! html/parsoid + !! end @@ -21895,14 +22104,19 @@ parsoid=wt2html,wt2wt,html2html # See: https://www.w3.org/TR/html5/syntax.html#character-references # Note that U+000C (form feed) is not a valid XML character, so # it is banned even though allowed in HTML5. +# Note there are also weird legacy numeric entities which are mapped +# elsewhere; see T113194 !! test -Illegal character references (T106578) +Illegal character references (T106578, T113194) +!! options +parsoid={ "modes": ["wt2html","html2html"], "normalizePhp": true } !! wikitext ; Null: � ; FF: ; CR: ; Control (low):  ; Control (high):  Ÿ +; Unsupported legacy: € ‚ ƒ – Ÿ ; Surrogate: �� ; This is an okay astral character: 💩 !! html+tidy @@ -21916,6 +22130,8 @@ Illegal character references (T106578)
    &#8;
    Control (high)
    &#x7F; &#x9F;
    +
    Unsupported legacy
    +
    &#128; &#130; &#131; &#150; &#159;
    Surrogate
    &#xD83D;&#xDCA9;
    This is an okay astral character
    @@ -22009,7 +22225,7 @@ T24905: followed by ISBN followed by

    (fr) ISBN 2753300917 example.com

    !! html/parsoid -

    (fr) ISBN 2753300917 example.com

    +

    (fr) ISBN 2753300917 example.com

    !! end !! test @@ -22151,7 +22367,7 @@ Images with the "|" character in the comment !! html/php
    An external URL
    !! html/parsoid -
    An external URL
    +
    An external URL
    !! end !! test @@ -23381,7 +23597,7 @@ Nested: -{zh-hans:Hi -{zh-cn:China;zh-sg:Singapore;}-;zh-hant:Hello -{zh-tw:Taiw

    Nested: Hello Hong Kong!

    !! html/parsoid -

    Nested: !

    +

    Nested: !

    !! end !! test @@ -23394,7 +23610,7 @@ language=zh variant=zh-cn

    A

    !! html/parsoid -

    +

    !! end !! test @@ -23407,7 +23623,7 @@ language=zh variant=zh-cn

    A

    !! html/parsoid -

    +

    !! end # Parsoid and PHP disagree on how to parse this example: Parsoid @@ -23552,13 +23768,13 @@ gopher://www.google.com www.гоогле.цом

    !! html/parsoid -

    http://www.google.com -gopher://www.google.com -http://www.google.com -gopher://www.google.com -irc://www.google.com -www.google.com/ftp://dir -www.google.com

    +

    http://www.google.com +gopher://www.google.com +http://www.google.com +gopher://www.google.com +irc://www.google.com +www.google.com/ftp://dir +www.google.com

    !! end !! test @@ -24245,7 +24461,7 @@ language=fa

    [Û±]

    !! html/parsoid -

    +

    !! end !! test @@ -25625,7 +25841,7 @@ T36939 - Case insensitive link parsing ([HttP://])

    [1]

    !! html/parsoid -

    +

    !! end !!test @@ -25645,7 +25861,7 @@ HttP://MediaWiki.Org/

    HttP://MediaWiki.Org/

    !! html/parsoid -

    HttP://MediaWiki.Org/

    +

    HttP://MediaWiki.Org/

    !! end !!test @@ -25779,6 +25995,17 @@ parsoid=wt2html,wt2wt
    !! end +## Just a regression test +!! test +Wikilink with only closing tag in target +!! options +parsoid=wt2html +!! wikitext +[[Test|]] +!! html/parsoid +

    +!! end + #### ---------------------------------------------------------------- #### Parsoid-only testing of Parsoid's impl of LST #### Not implemented yet, see @@ -28417,7 +28644,7 @@ parsoid=html2wt !!end !! test -Don't block XML namespace declaration +T72867: Don't block XML namespace declaration !! wikitext MediaWiki !! html/php @@ -28591,6 +28818,23 @@ parsoid=html2wt [[es:Toxine_bactérienne]] !! end +# Regression test for T219023 +!! test +Emit simple non-piped link where possible +!! options +parsoid=html2wt +!! html/parsoid +VisualEditor +visualEditor +VisualEditor link +visualEditor link +!! wikitext +[[VisualEditor]] +[[visualEditor]] +[[VisualEditor link]] +[[visualEditor link]] +!! end + !! test Image: Modifying size of an image (1) !! options @@ -29604,7 +29848,7 @@ WTS of autolinks with nowikis (round-trip) !! wikitext xhttp://cscott.netx !! html/parsoid -

    xhttp://cscott.netx

    +

    xhttp://cscott.netx

    !! end # this is the "easy" test because it leaves in place all the @@ -29659,6 +29903,58 @@ parsoid=html2wt http://example.com http://example.com is not a link. !! end +!! test +WTS of an autolink surrounded by square brackets (T220018) +!! options +parsoid=html2wt +!! html/parsoid +

    [http://example.com]

    +!! wikitext +[http://example.com] +!! end + +!! test +WTS of edited autolink surrounded by square brackets (T220018) +!! options +parsoid={ + "modes": ["wt2wt"], + "changes": [ + [ "a", "before", "[" ], + [ "a", "after", "]" ] + ] +} +!! wikitext +http://example.com +!! wikitext/edited +[http://example.com] +!! end + +!! test +WTS of an external link surrounded by square brackets (T220018) +!! options +parsoid=html2wt +!! html/parsoid +

    [foo]

    +!! wikitext +[[http://example.com foo]] +!! end + +!! test +WTS of edited external link surrounded by square brackets (T220018) +!! options +parsoid={ + "modes": ["wt2wt"], + "changes": [ + [ "a", "before", "[" ], + [ "a", "after", "]" ] + ] +} +!! wikitext +[http://example.com foo] +!! wikitext/edited +[[http://example.com foo]] +!! end + !! test Magic links inside links (not autolinked) !! wikitext @@ -29687,10 +29983,10 @@ Magic links inside links (not autolinked) PMID 1234 ISBN 123456789x

    -

    http://example.com -RFC 1234 -PMID 1234 -ISBN 123456789x

    +

    http://example.com +RFC 1234 +PMID 1234 +ISBN 123456789x

    !! end !! test @@ -29706,7 +30002,7 @@ Magic links inside image captions (autolinked) !! html/parsoid -
    http://example.com
    +
    http://example.com
    RFC 1234
    PMID 1234
    ISBN 123456789x
    @@ -30136,7 +30432,7 @@ parsoid=wt2html !! wikitext {{echo|hi}}[http://example.com [[ho]]] !! html/parsoid -

    hiho

    +

    hiho

    !! end !! test @@ -30152,7 +30448,7 @@ Use data-parsoid.firstWikitextNode to compute newline constraints for template c !! options parsoid=html2wt !! html/parsoid -a +a
    d
    @@ -30211,9 +30507,9 @@ parsoid={

    123
    !! html/parsoid -

    +

    -
    123
    +
    123
    !! end # -------------------------------------------- @@ -31216,6 +31512,20 @@ parsoid=html2wt {{echo|foo}} !! end +!! test +Only html p-tag is strong indent pre suppressing +!! options +parsoid=html2wt +!! html/parsoid +

    test2 + test3 +

    +!! wikitext +test2 + test3 + +!! end + # ----------------------------------------------------------------- # End of section for Parsoid-only html2wt tests for serialization # of new content @@ -31771,8 +32081,8 @@ T51672: Test for brackets in attributes of elements in external link texts link span

    !! html/parsoid -

    link span -link span

    +

    link span +link span

    !! end !! test @@ -32869,3 +33179,13 @@ header *foo footer !! end + +!! test +Ensure disambiguation links are marked properly +!! options +parsoid=wt2html +!! wikitext +[[Disambiguation]] +!! html/parsoid +

    Disambiguation

    +!! end diff --git a/tests/phpunit/includes/Permissions/PermissionManagerTest.php b/tests/phpunit/includes/Permissions/PermissionManagerTest.php index 122f377674..44b7f67a31 100644 --- a/tests/phpunit/includes/Permissions/PermissionManagerTest.php +++ b/tests/phpunit/includes/Permissions/PermissionManagerTest.php @@ -1865,4 +1865,26 @@ class PermissionManagerTest extends MediaWikiLangTestCase { ->getPermissionManager() ->getNamespaceRestrictionLevels( $ns, $user ) ); } + + /** + * @covers \MediaWiki\Permissions\PermissionManager::getRightsCacheKey + * @throws \Exception + */ + public function testAnonPermissionsNotClash() { + $user1 = User::newFromName( 'User1' ); + $user2 = User::newFromName( 'User2' ); + $pm = MediaWikiServices::getInstance()->getPermissionManager(); + $pm->overrideUserRightsForTesting( $user2, [] ); + $this->assertNotSame( $pm->getUserPermissions( $user1 ), $pm->getUserPermissions( $user2 ) ); + } + + /** + * @covers \MediaWiki\Permissions\PermissionManager::getRightsCacheKey + */ + public function testAnonPermissionsNotClashOneRegistered() { + $user1 = User::newFromName( 'User1' ); + $user2 = $this->getTestSysop()->getUser(); + $pm = MediaWikiServices::getInstance()->getPermissionManager(); + $this->assertNotSame( $pm->getUserPermissions( $user1 ), $pm->getUserPermissions( $user2 ) ); + } } diff --git a/tests/phpunit/includes/filebackend/FileBackendGroupIntegrationTest.php b/tests/phpunit/includes/filebackend/FileBackendGroupIntegrationTest.php new file mode 100644 index 0000000000..ee3262c4e8 --- /dev/null +++ b/tests/phpunit/includes/filebackend/FileBackendGroupIntegrationTest.php @@ -0,0 +1,57 @@ +getLockManagerGroupFactory(); + } + + private function newObj( array $options = [] ) : FileBackendGroup { + $globals = [ 'DirectoryMode', 'FileBackends', 'ForeignFileRepos', 'LocalFileRepo' ]; + foreach ( $globals as $global ) { + $this->setMwGlobals( + "wg$global", $options[$global] ?? self::getDefaultOptions()[$global] ); + } + + $serviceMembers = [ + 'configuredROMode' => 'ConfiguredReadOnlyMode', + 'srvCache' => 'LocalServerObjectCache', + 'wanCache' => 'MainWANObjectCache', + 'mimeAnalyzer' => 'MimeAnalyzer', + 'lmgFactory' => 'LockManagerGroupFactory', + 'tmpFileFactory' => 'TempFSFileFactory', + ]; + + foreach ( $serviceMembers as $key => $name ) { + if ( isset( $options[$key] ) ) { + $this->setService( $name, $options[$key] ); + } + } + + $this->assertEmpty( + array_diff( array_keys( $options ), $globals, array_keys( $serviceMembers ) ) ); + + $this->resetServices(); + FileBackendGroup::destroySingleton(); + + $services = MediaWikiServices::getInstance(); + + foreach ( $serviceMembers as $key => $name ) { + $this->$key = $services->getService( $name ); + } + + return FileBackendGroup::singleton(); + } +} diff --git a/tests/phpunit/includes/user/UserTest.php b/tests/phpunit/includes/user/UserTest.php index f48385d17a..9ff052142f 100644 --- a/tests/phpunit/includes/user/UserTest.php +++ b/tests/phpunit/includes/user/UserTest.php @@ -929,13 +929,8 @@ class UserTest extends MediaWikiTestCase { $this->assertFalse( $user->isPingLimitable() ); $this->setMwGlobals( 'wgRateLimitsExcludedIPs', [] ); - $noRateLimitUser = $this->getMockBuilder( User::class )->disableOriginalConstructor() - ->setMethods( [ 'getIP', 'getId', 'getGroups' ] )->getMock(); - $noRateLimitUser->expects( $this->any() )->method( 'getIP' )->willReturn( '1.2.3.4' ); - $noRateLimitUser->expects( $this->any() )->method( 'getId' )->willReturn( 0 ); - $noRateLimitUser->expects( $this->any() )->method( 'getGroups' )->willReturn( [] ); - $this->overrideUserPermissions( $noRateLimitUser, 'noratelimit' ); - $this->assertFalse( $noRateLimitUser->isPingLimitable() ); + $this->overrideUserPermissions( $user, 'noratelimit' ); + $this->assertFalse( $user->isPingLimitable() ); } public function provideExperienceLevel() { diff --git a/tests/phpunit/unit/includes/filebackend/FileBackendGroupTestTrait.php b/tests/phpunit/unit/includes/filebackend/FileBackendGroupTestTrait.php new file mode 100644 index 0000000000..d23f645eeb --- /dev/null +++ b/tests/phpunit/unit/includes/filebackend/FileBackendGroupTestTrait.php @@ -0,0 +1,459 @@ + LocalRepo::class, + 'name' => 'local', + 'directory' => 'upload-dir', + 'thumbDir' => 'thumb/', + 'transcodedDir' => 'transcoded/', + 'fileMode' => 0664, + 'scriptDirUrl' => 'script-path/', + 'url' => 'upload-path/', + 'hashLevels' => 2, + 'thumbScriptUrl' => false, + 'transformVia404' => false, + 'deletedDir' => 'deleted/', + 'deletedHashLevels' => 3, + 'backend' => 'local-backend', + ]; + } + + private static function getDefaultOptions() { + return [ + 'DirectoryMode' => 0775, + 'FileBackends' => [], + 'ForeignFileRepos' => [], + 'LocalFileRepo' => self::getDefaultLocalFileRepo(), + 'wikiId' => self::getWikiID(), + ]; + } + + /** + * @covers ::__construct + */ + public function testConstructor_overrideImplicitBackend() { + $obj = $this->newObj( [ 'FileBackends' => + [ [ 'name' => 'local-backend', 'class' => '', 'lockManager' => 'fsLockManager' ] ] + ] ); + $this->assertSame( '', $obj->config( 'local-backend' )['class'] ); + } + + /** + * @covers ::__construct + */ + public function testConstructor_backendObject() { + // 'backend' being an object makes that repo from configuration ignored + // XXX This is not documented in DefaultSettings.php, does it do anything useful? + $obj = $this->newObj( [ 'ForeignFileRepos' => [ [ 'backend' => new stdclass ] ] ] ); + $this->assertSame( FSFileBackend::class, $obj->config( 'local-backend' )['class'] ); + } + + /** + * @dataProvider provideRegister_domainId + * @param string $key Key to check in return value of config() + * @param string|callable $expected Expected value of config()[$key], or callable returning it + * @param array $extraBackendsOptions To add to the FileBackends entry passed to newObj() + * @param array $otherExtraOptions To add to the array passed to newObj() (e.g., services) + * @covers ::register + */ + public function testRegister( + $key, $expected, array $extraBackendsOptions = [], array $otherExtraOptions = [] + ) { + if ( $expected instanceof Closure ) { + // Lame hack to get around providers being called too early + $expected = $expected(); + } + if ( $key === 'domainId' ) { + // This will change the expected LMG name too + $otherExtraOptions['lmgFactory'] = $this->getLockManagerGroupFactory( $expected ); + } + $obj = $this->newObj( $otherExtraOptions + [ + 'FileBackends' => [ + $extraBackendsOptions + [ + 'name' => 'myname', 'class' => '', 'lockManager' => 'fsLockManager' + ] + ], + ] ); + $this->assertSame( $expected, $obj->config( 'myname' )[$key] ); + } + + public static function provideRegister_domainId() { + return [ + 'domainId with neither wikiId nor domainId set' => [ + 'domainId', + function () { + return self::getWikiID(); + }, + ], + 'domainId with wikiId set but no domainId' => + [ 'domainId', 'id0', [ 'wikiId' => 'id0' ] ], + 'domainId with wikiId and domainId set' => + [ 'domainId', 'dom1', [ 'wikiId' => 'id0', 'domainId' => 'dom1' ] ], + 'readOnly without readOnly set' => [ 'readOnly', false ], + 'readOnly with readOnly set to string' => + [ 'readOnly', 'cuz', [ 'readOnly' => 'cuz' ] ], + 'readOnly without readOnly set but with string in passed object' => [ + 'readOnly', + 'cuz', + [], + [ 'configuredROMode' => new ConfiguredReadOnlyMode( 'cuz' ) ], + ], + 'readOnly with readOnly set to false but string in passed object' => [ + 'readOnly', + false, + [ 'readOnly' => false ], + [ 'configuredROMode' => new ConfiguredReadOnlyMode( 'cuz' ) ], + ], + ]; + } + + /** + * @dataProvider provideRegister_exception + * @param array $fileBackends Value of FileBackends to pass to constructor + * @param string $class Expected exception class + * @param string $msg Expected exception message + * @covers ::__construct + * @covers ::register + */ + public function testRegister_exception( $fileBackends, $class, $msg ) { + $this->setExpectedException( $class, $msg ); + $this->newObj( [ 'FileBackends' => $fileBackends ] ); + } + + public static function provideRegister_exception() { + return [ + 'Nameless' => [ + [ [] ], InvalidArgumentException::class, "Cannot register a backend with no name." + ], + 'Duplicate' => [ + [ [ 'name' => 'dupe', 'class' => '' ], [ 'name' => 'dupe' ] ], + LogicException::class, + "Backend with name 'dupe' already registered.", + ], + 'Classless' => [ + [ [ 'name' => 'classless' ] ], + InvalidArgumentException::class, + "Backend with name 'classless' has no class.", + ], + ]; + } + + /** + * @covers ::__construct + * @covers ::config + * @covers ::get + */ + public function testGet() { + $backend = $this->newObj()->get( 'local-backend' ); + $this->assertTrue( $backend instanceof FSFileBackend ); + } + + /** + * @covers ::get + */ + public function testGetUnrecognized() { + $this->setExpectedException( InvalidArgumentException::class, + "No backend defined with the name 'unrecognized'." ); + $this->newObj()->get( 'unrecognized' ); + } + + /** + * @covers ::__construct + * @covers ::config + */ + public function testConfig() { + $obj = $this->newObj(); + $config = $obj->config( 'local-backend' ); + + // XXX How to actually test that a profiler is loaded? + $this->assertNull( $config['profiler']( 'x' ) ); + // Equality comparison doesn't work for closures, so just set to null + $config['profiler'] = null; + + $this->assertEquals( [ + 'mimeCallback' => [ $obj, 'guessMimeInternal' ], + 'obResetFunc' => 'wfResetOutputBuffers', + 'streamMimeFunc' => [ StreamFile::class, 'contentTypeFromPath' ], + 'tmpFileFactory' => $this->tmpFileFactory, + 'statusWrapper' => [ Status::class, 'wrap' ], + 'wanCache' => $this->wanCache, + 'srvCache' => $this->srvCache, + 'logger' => LoggerFactory::getInstance( 'FileOperation' ), + // This was set to null above in $config, it's not really null + 'profiler' => null, + 'name' => 'local-backend', + 'containerPaths' => [ + 'local-public' => 'upload-dir', + 'local-thumb' => 'thumb/', + 'local-transcoded' => 'transcoded/', + 'local-deleted' => 'deleted/', + 'local-temp' => 'upload-dir/temp', + ], + 'fileMode' => 0664, + 'directoryMode' => 0775, + 'domainId' => self::getWikiID(), + 'readOnly' => false, + 'class' => FSFileBackend::class, + 'lockManager' => + $this->lmgFactory->getLockManagerGroup( self::getWikiID() )->get( 'fsLockManager' ), + 'fileJournal' => + FileJournal::factory( [ 'class' => NullFileJournal::class ], 'local-backend' ), + ], $config ); + + // For config values that are objects, check object identity. + $this->assertSame( [ $obj, 'guessMimeInternal' ], $config['mimeCallback'] ); + $this->assertSame( $this->tmpFileFactory, $config['tmpFileFactory'] ); + $this->assertSame( $this->wanCache, $config['wanCache'] ); + $this->assertSame( $this->srvCache, $config['srvCache'] ); + } + + /** + * @dataProvider provideConfig_default + * @param string $expected Expected default value + * @param string $inputName Name to set to null in LocalFileRepo setting + * @param string|array $key Key to check in array returned by config(), or array [ 'key1', + * 'key2' ] for nested key + * @covers ::__construct + * @covers ::config + */ + public function testConfig_defaultNull( $expected, $inputName, $key ) { + $config = self::getDefaultLocalFileRepo(); + $config[$inputName] = null; + + $result = $this->newObj( [ 'LocalFileRepo' => $config ] )->config( 'local-backend' ); + + $actual = is_string( $key ) ? $result[$key] : $result[$key[0]][$key[1]]; + + $this->assertSame( $expected, $actual ); + } + + /** + * @dataProvider provideConfig_default + * @param string $expected Expected default value + * @param string $inputName Name to unset in LocalFileRepo setting + * @param string|array $key Key to check in array returned by config(), or array [ 'key1', + * 'key2' ] for nested key + * @covers ::__construct + * @covers ::config + */ + public function testConfig_defaultUnset( $expected, $inputName, $key ) { + $config = self::getDefaultLocalFileRepo(); + unset( $config[$inputName] ); + + $result = $this->newObj( [ 'LocalFileRepo' => $config ] )->config( 'local-backend' ); + + $actual = is_string( $key ) ? $result[$key] : $result[$key[0]][$key[1]]; + + $this->assertSame( $expected, $actual ); + } + + public static function provideConfig_default() { + return [ + 'deletedDir' => [ false, 'deletedDir', [ 'containerPaths', 'local-deleted' ] ], + 'thumbDir' => [ 'upload-dir/thumb', 'thumbDir', [ 'containerPaths', 'local-thumb' ] ], + 'transcodedDir' => [ + 'upload-dir/transcoded', 'transcodedDir', [ 'containerPaths', 'local-transcoded' ] + ], + 'fileMode' => [ 0644, 'fileMode', 'fileMode' ], + ]; + } + + /** + * @covers ::config + */ + public function testConfig_fileJournal() { + $mockJournal = $this->createMock( FileJournal::class ); + $mockJournal->expects( $this->never() )->method( $this->anything() ); + + $obj = $this->newObj( [ 'FileBackends' => [ [ + 'name' => 'name', + 'class' => '', + 'lockManager' => 'fsLockManager', + 'fileJournal' => [ 'factory' => + function () use ( $mockJournal ) { + return $mockJournal; + } + ], + ] ] ] ); + + $this->assertSame( $mockJournal, $obj->config( 'name' )['fileJournal'] ); + } + + /** + * @covers ::config + */ + public function testConfigUnrecognized() { + $this->setExpectedException( InvalidArgumentException::class, + "No backend defined with the name 'unrecognized'." ); + $this->newObj()->config( 'unrecognized' ); + } + + /** + * @dataProvider provideBackendFromPath + * @covers ::backendFromPath + * @param string|null $expected Name of backend that will be returned from 'get', or null + * @param string $storagePath + */ + public function testBackendFromPath( $expected = null, $storagePath ) { + $obj = $this->newObj( [ 'FileBackends' => [ + [ 'name' => '', 'class' => stdclass::class, 'lockManager' => 'fsLockManager' ], + [ 'name' => 'a', 'class' => stdclass::class, 'lockManager' => 'fsLockManager' ], + [ 'name' => 'b', 'class' => stdclass::class, 'lockManager' => 'fsLockManager' ], + ] ] ); + $this->assertSame( + $expected === null ? null : $obj->get( $expected ), + $obj->backendFromPath( $storagePath ) + ); + } + + public static function provideBackendFromPath() { + return [ + 'Empty string' => [ null, '' ], + 'mwstore://' => [ null, 'mwstore://' ], + 'mwstore://a' => [ null, 'mwstore://a' ], + 'mwstore:///' => [ null, 'mwstore:///' ], + 'mwstore://a/' => [ null, 'mwstore://a/' ], + 'mwstore://a//' => [ null, 'mwstore://a//' ], + 'mwstore://a/b' => [ 'a', 'mwstore://a/b' ], + 'mwstore://a/b/' => [ 'a', 'mwstore://a/b/' ], + 'mwstore://a/b////' => [ 'a', 'mwstore://a/b////' ], + 'mwstore://a/b/c' => [ 'a', 'mwstore://a/b/c' ], + 'mwstore://a/b/c/d' => [ 'a', 'mwstore://a/b/c/d' ], + 'mwstore://b/b' => [ 'b', 'mwstore://b/b' ], + 'mwstore://c/b' => [ null, 'mwstore://c/b' ], + ]; + } + + /** + * @dataProvider provideGuessMimeInternal + * @covers ::guessMimeInternal + * @param string $storagePath + * @param string|null $content + * @param string|null $fsPath + * @param string|null $expectedExtensionType Expected return of + * MimeAnalyzer::guessTypesForExtension + * @param string|null $expectedGuessedMimeType Expected return value of + * MimeAnalyzer::guessMimeType (null if expected not to be called) + */ + public function testGuessMimeInternal( + $storagePath, + $content, + $fsPath, + $expectedExtensionType, + $expectedGuessedMimeType + ) { + $mimeAnalyzer = $this->createMock( MimeAnalyzer::class ); + $mimeAnalyzer->expects( $this->once() )->method( 'guessTypesForExtension' ) + ->willReturn( $expectedExtensionType ); + $tmpFileFactory = $this->createMock( TempFSFileFactory::class ); + + if ( !$expectedExtensionType && $fsPath ) { + $tmpFileFactory->expects( $this->never() )->method( 'newTempFSFile' ); + $mimeAnalyzer->expects( $this->once() )->method( 'guessMimeType' ) + ->with( $fsPath, false )->willReturn( $expectedGuessedMimeType ); + } elseif ( !$expectedExtensionType && strlen( $content ) ) { + // XXX What should we do about the file creation here? Really we should mock + // file_put_contents() somehow. It's not very nice to ignore the value of + // $wgTmpDirectory. + $tmpFile = ( new TempFSFileFactory() )->newTempFSFile( 'mime_', '' ); + + $tmpFileFactory->expects( $this->once() )->method( 'newTempFSFile' ) + ->with( 'mime_', '' )->willReturn( $tmpFile ); + $mimeAnalyzer->expects( $this->once() )->method( 'guessMimeType' ) + ->with( $tmpFile->getPath(), false )->willReturn( $expectedGuessedMimeType ); + } else { + $tmpFileFactory->expects( $this->never() )->method( 'newTempFSFile' ); + $mimeAnalyzer->expects( $this->never() )->method( 'guessMimeType' ); + } + + $mimeAnalyzer->expects( $this->never() ) + ->method( $this->anythingBut( 'guessTypesForExtension', 'guessMimeType' ) ); + $tmpFileFactory->expects( $this->never() ) + ->method( $this->anythingBut( 'newTempFSFile' ) ); + + $obj = $this->newObj( [ + 'mimeAnalyzer' => $mimeAnalyzer, + 'tmpFileFactory' => $tmpFileFactory, + ] ); + + $this->assertSame( $expectedExtensionType ?? $expectedGuessedMimeType ?? 'unknown/unknown', + $obj->guessMimeInternal( $storagePath, $content, $fsPath ) ); + } + + public static function provideGuessMimeInternal() { + return [ + 'With extension' => + [ 'foo.txt', null, null, 'text/plain', null ], + 'No extension' => + [ 'foo', null, null, null, null ], + 'Empty content, with extension' => + [ 'foo.txt', '', null, 'text/plain', null ], + 'Empty content, no extension' => + [ 'foo', '', null, null, null ], + 'Non-empty content, with extension' => + [ 'foo.txt', 'foo', null, 'text/plain', null ], + 'Non-empty content, no extension' => + [ 'foo', 'foo', null, null, 'text/html' ], + 'Empty path, with extension' => + [ 'foo.txt', null, '', 'text/plain', null ], + 'Empty path, no extension' => + [ 'foo', null, '', null, null ], + 'Non-empty path, with extension' => + [ 'foo.txt', null, '/bogus/path', 'text/plain', null ], + 'Non-empty path, no extension' => + [ 'foo', null, '/bogus/path', null, 'text/html' ], + 'Empty path and content, with extension' => + [ 'foo.txt', '', '', 'text/plain', null ], + 'Empty path and content, no extension' => + [ 'foo', '', '', null, null ], + 'Non-empty path and content, with extension' => + [ 'foo.txt', 'foo', '/bogus/path', 'text/plain', null ], + 'Non-empty path and content, no extension' => + [ 'foo', 'foo', '/bogus/path', null, 'image/jpeg' ], + ]; + } +} diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.Uri.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.Uri.test.js index 013fb0d065..9230ab734f 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.Uri.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.Uri.test.js @@ -105,6 +105,18 @@ 'Parse an ftp URI correctly with user and password' ); + uri = new mw.Uri( 'http://example.com/?foo[1]=b&foo[0]=a&foo[]=c' ); + + assert.deepEqual( + uri.query, + { + 'foo[1]': 'b', + 'foo[0]': 'a', + 'foo[]': 'c' + }, + 'Array query parameters parsed as normal with arrayParams:false' + ); + assert.throws( function () { return new mw.Uri( 'glaswegian penguins' ); diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js index 894dd194ca..3258f8ea84 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js @@ -21,6 +21,9 @@ window.Set = this.nativeSet; mw.redefineFallbacksForTest(); } + if ( this.resetStoreKey ) { + localStorage.removeItem( mw.loader.store.key ); + } // Remove any remaining temporary statics // exposed for cross-file mocks. delete mw.loader.testCallback; @@ -1049,6 +1052,78 @@ } ); } ); + QUnit.test( 'mw.loader.store.init - Invalid JSON', function ( assert ) { + // Reset + this.sandbox.stub( mw.loader.store, 'enabled', null ); + this.sandbox.stub( mw.loader.store, 'items', {} ); + this.resetStoreKey = true; + localStorage.setItem( mw.loader.store.key, 'invalid' ); + + mw.loader.store.init(); + assert.strictEqual( mw.loader.store.enabled, true, 'Enabled' ); + assert.strictEqual( + $.isEmptyObject( mw.loader.store.items ), + true, + 'Items starts fresh' + ); + } ); + + QUnit.test( 'mw.loader.store.init - Wrong JSON', function ( assert ) { + // Reset + this.sandbox.stub( mw.loader.store, 'enabled', null ); + this.sandbox.stub( mw.loader.store, 'items', {} ); + this.resetStoreKey = true; + localStorage.setItem( mw.loader.store.key, JSON.stringify( { wrong: true } ) ); + + mw.loader.store.init(); + assert.strictEqual( mw.loader.store.enabled, true, 'Enabled' ); + assert.strictEqual( + $.isEmptyObject( mw.loader.store.items ), + true, + 'Items starts fresh' + ); + } ); + + QUnit.test( 'mw.loader.store.init - Expired JSON', function ( assert ) { + // Reset + this.sandbox.stub( mw.loader.store, 'enabled', null ); + this.sandbox.stub( mw.loader.store, 'items', {} ); + this.resetStoreKey = true; + localStorage.setItem( mw.loader.store.key, JSON.stringify( { + items: { use: 'not me' }, + vary: mw.loader.store.vary, + asOf: 130161 // 2011-04-01 12:00 + } ) ); + + mw.loader.store.init(); + assert.strictEqual( mw.loader.store.enabled, true, 'Enabled' ); + assert.strictEqual( + $.isEmptyObject( mw.loader.store.items ), + true, + 'Items starts fresh' + ); + } ); + + QUnit.test( 'mw.loader.store.init - Good JSON', function ( assert ) { + // Reset + this.sandbox.stub( mw.loader.store, 'enabled', null ); + this.sandbox.stub( mw.loader.store, 'items', {} ); + this.resetStoreKey = true; + localStorage.setItem( mw.loader.store.key, JSON.stringify( { + items: { use: 'me' }, + vary: mw.loader.store.vary, + asOf: Math.ceil( Date.now() / 1e7 ) - 5 // ~ 13 hours ago + } ) ); + + mw.loader.store.init(); + assert.strictEqual( mw.loader.store.enabled, true, 'Enabled' ); + assert.deepEqual( + mw.loader.store.items, + { use: 'me' }, + 'Stored items are loaded' + ); + } ); + QUnit.test( 'require()', function ( assert ) { mw.loader.register( [ [ 'test.require1', '0' ],