Merge "Allow passing the default DB group to use in Maintenance scripts"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 19 Jun 2018 05:22:44 +0000 (05:22 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 19 Jun 2018 05:22:44 +0000 (05:22 +0000)
244 files changed:
HISTORY
RELEASE-NOTES-1.31 [deleted file]
RELEASE-NOTES-1.32
autoload.php
composer.json
docs/hooks.txt
docs/pageupdater.txt [new file with mode: 0644]
includes/AuthPlugin.php
includes/Category.php
includes/DefaultSettings.php
includes/DummyLinker.php
includes/EditPage.php
includes/Linker.php
includes/MWNamespace.php
includes/MediaWiki.php
includes/OutputPage.php
includes/Revision.php
includes/ServiceWiring.php
includes/Storage/DerivedPageDataUpdater.php [new file with mode: 0644]
includes/Storage/MutableRevisionRecord.php
includes/Storage/MutableRevisionSlots.php
includes/Storage/NameTableStore.php
includes/Storage/PageUpdateException.php [new file with mode: 0644]
includes/Storage/PageUpdater.php [new file with mode: 0644]
includes/Storage/RevisionRecord.php
includes/Storage/RevisionSlots.php
includes/Storage/RevisionSlotsUpdate.php
includes/Storage/RevisionStore.php
includes/Storage/SlotRecord.php
includes/Title.php
includes/WebResponse.php
includes/XmlSelect.php
includes/api/ApiPageSet.php
includes/api/ApiParse.php
includes/api/ApiQueryPrefixSearch.php
includes/api/ApiQuerySearch.php
includes/api/SearchApi.php
includes/api/i18n/ar.json
includes/api/i18n/de.json
includes/api/i18n/en.json
includes/api/i18n/fr.json
includes/api/i18n/he.json
includes/api/i18n/it.json
includes/api/i18n/ko.json
includes/api/i18n/ksh.json
includes/api/i18n/pl.json
includes/api/i18n/pt-br.json
includes/api/i18n/pt.json
includes/api/i18n/qqq.json
includes/api/i18n/ru.json
includes/api/i18n/zh-hans.json
includes/auth/AuthManager.php
includes/cache/LinkCache.php
includes/cache/localisation/LocalisationCache.php
includes/changes/RecentChange.php
includes/changetags/ChangeTags.php
includes/collation/IcuCollation.php
includes/deferred/LinksDeletionUpdate.php
includes/edit/PreparedEdit.php
includes/filerepo/FileRepo.php
includes/filerepo/RepoGroup.php
includes/filerepo/file/LocalFile.php
includes/htmlform/fields/HTMLCheckMatrix.php
includes/installer/i18n/eu.json
includes/installer/i18n/hu.json
includes/libs/MultiHttpClient.php
includes/libs/rdbms/ChronologyProtector.php
includes/libs/rdbms/database/DatabaseMysqlBase.php
includes/libs/rdbms/lbfactory/LBFactory.php
includes/libs/stats/BufferingStatsdDataFactory.php
includes/linker/LinkRenderer.php
includes/media/FormatMetadata.php
includes/page/WikiPage.php
includes/parser/CoreParserFunctions.php
includes/parser/LinkHolderArray.php
includes/parser/Parser.php
includes/parser/ParserOptions.php
includes/preferences/DefaultPreferencesFactory.php
includes/resourceloader/ResourceLoaderClientHtml.php
includes/resourceloader/ResourceLoaderWikiModule.php
includes/search/PaginatingSearchEngine.php [new file with mode: 0644]
includes/search/SearchEngine.php
includes/search/SearchNearMatchResultSet.php
includes/search/SearchResult.php
includes/search/SearchResultSet.php
includes/search/SearchSuggestionSet.php
includes/search/SqlSearchResultSet.php
includes/services/ServiceContainer.php
includes/session/PHPSessionHandler.php
includes/skins/QuickTemplate.php
includes/skins/Skin.php
includes/specials/SpecialComparePages.php
includes/upload/UploadStash.php
includes/user/User.php
includes/widget/search/BasicSearchResultSetWidget.php
includes/widget/search/SimpleSearchResultSetWidget.php
languages/Language.php
languages/i18n/ace.json
languages/i18n/ar.json
languages/i18n/ast.json
languages/i18n/be-tarask.json
languages/i18n/be.json
languages/i18n/bn.json
languages/i18n/br.json
languages/i18n/btm.json
languages/i18n/ca.json
languages/i18n/ce.json
languages/i18n/ckb.json
languages/i18n/cs.json
languages/i18n/cv.json
languages/i18n/de.json
languages/i18n/el.json
languages/i18n/en.json
languages/i18n/eo.json
languages/i18n/es.json
languages/i18n/eu.json
languages/i18n/fr.json
languages/i18n/gcr.json
languages/i18n/gl.json
languages/i18n/got.json
languages/i18n/he.json
languages/i18n/hr.json
languages/i18n/hu.json
languages/i18n/inh.json
languages/i18n/io.json
languages/i18n/it.json
languages/i18n/ko.json
languages/i18n/ku-latn.json
languages/i18n/kum.json
languages/i18n/lb.json
languages/i18n/lt.json
languages/i18n/lv.json
languages/i18n/mg.json
languages/i18n/mk.json
languages/i18n/ml.json
languages/i18n/nap.json
languages/i18n/nb.json
languages/i18n/nds-nl.json
languages/i18n/nl.json
languages/i18n/pl.json
languages/i18n/pms.json
languages/i18n/pt-br.json
languages/i18n/pt.json
languages/i18n/qqq.json
languages/i18n/ru.json
languages/i18n/sd.json
languages/i18n/sk.json
languages/i18n/sl.json
languages/i18n/sr-ec.json
languages/i18n/sv.json
languages/i18n/tet.json
languages/i18n/tr.json
languages/i18n/tt-cyrl.json
languages/i18n/ur.json
languages/i18n/vi.json
languages/i18n/war.json
languages/i18n/yi.json
languages/i18n/zgh.json
languages/i18n/zh-hans.json
languages/i18n/zh-hant.json
maintenance/populateChangeTagDef.php [new file with mode: 0644]
maintenance/refreshLinks.php
maintenance/resources/update-oojs.sh
package.json
resources/Resources.php
resources/lib/oojs/README.md
resources/lib/oojs/oojs.jquery.js
resources/src/jquery.tablesorter/jquery.tablesorter.less
resources/src/jquery/jquery.tablesorter.styles.less [deleted file]
resources/src/mediawiki.jqueryMsg/mediawiki.jqueryMsg.js
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterTagMultiselectWidget.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.SavedLinksListWidget.less
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterTagMultiselectWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.SavedLinksListItemWidget.js
resources/src/mediawiki.special.contributions.js
resources/src/mediawiki.ui/components/inputs.less
resources/src/mediawiki/mediawiki.base.js
resources/src/mediawiki/mediawiki.js
tests/common/TestsAutoLoader.php
tests/parser/ParserTestRunner.php
tests/phpunit/MediaWikiTestCase.php
tests/phpunit/includes/LinkerTest.php
tests/phpunit/includes/MediaWikiTest.php
tests/phpunit/includes/RevisionDbTestBase.php
tests/phpunit/includes/RevisionMcrDbTest.php [new file with mode: 0644]
tests/phpunit/includes/RevisionMcrWriteBothDbTest.php [new file with mode: 0644]
tests/phpunit/includes/RevisionTest.php
tests/phpunit/includes/Storage/DerivedPageDataUpdaterTest.php [new file with mode: 0644]
tests/phpunit/includes/Storage/McrRevisionStoreDbTest.php [new file with mode: 0644]
tests/phpunit/includes/Storage/McrSchemaOverride.php [new file with mode: 0644]
tests/phpunit/includes/Storage/McrWriteBothRevisionStoreDbTest.php [new file with mode: 0644]
tests/phpunit/includes/Storage/McrWriteBothSchemaOverride.php [new file with mode: 0644]
tests/phpunit/includes/Storage/MutableRevisionRecordTest.php
tests/phpunit/includes/Storage/MutableRevisionSlotsTest.php
tests/phpunit/includes/Storage/NameTableStoreTest.php
tests/phpunit/includes/Storage/NoContentModelRevisionStoreDbTest.php
tests/phpunit/includes/Storage/PageUpdaterTest.php [new file with mode: 0644]
tests/phpunit/includes/Storage/PreMcrRevisionStoreDbTest.php
tests/phpunit/includes/Storage/RevisionSlotsTest.php
tests/phpunit/includes/Storage/RevisionSlotsUpdateTest.php
tests/phpunit/includes/Storage/RevisionStoreDbTestBase.php
tests/phpunit/includes/Storage/RevisionStoreRecordTest.php
tests/phpunit/includes/Storage/RevisionStoreTest.php
tests/phpunit/includes/Storage/SlotRecordTest.php
tests/phpunit/includes/Storage/drop-pre-mcr-fields.sql [new file with mode: 0644]
tests/phpunit/includes/Storage/drop-pre-mcr-fields.sqlite.sql [new file with mode: 0644]
tests/phpunit/includes/TitleMethodsTest.php
tests/phpunit/includes/TitleTest.php
tests/phpunit/includes/api/ApiBaseTest.php
tests/phpunit/includes/api/ApiQueryPrefixSearchTest.php [new file with mode: 0644]
tests/phpunit/includes/api/ApiQuerySearchTest.php [new file with mode: 0644]
tests/phpunit/includes/api/ApiQueryWatchlistIntegrationTest.php
tests/phpunit/includes/changetags/ChangeTagsTest.php
tests/phpunit/includes/content/ContentHandlerTest.php
tests/phpunit/includes/db/LBFactoryTest.php
tests/phpunit/includes/linker/LinkRendererTest.php
tests/phpunit/includes/page/WikiPageDbTestBase.php
tests/phpunit/includes/page/WikiPageMcrDbTest.php [new file with mode: 0644]
tests/phpunit/includes/page/WikiPageMcrWriteBothDbTest.php [new file with mode: 0644]
tests/phpunit/includes/resourceloader/MessageBlobStoreTest.php
tests/phpunit/includes/search/SearchEnginePrefixTest.php
tests/phpunit/includes/search/SearchEngineTest.php
tests/phpunit/includes/search/SearchNearMatchResultSetTest.php [new file with mode: 0644]
tests/phpunit/includes/search/SearchResultSetTest.php [new file with mode: 0644]
tests/phpunit/includes/search/SearchResultTest.php [new file with mode: 0644]
tests/phpunit/includes/specials/SpecialSearchTest.php
tests/phpunit/includes/user/UserTest.php
tests/phpunit/languages/SpecialPageAliasTest.php
tests/phpunit/maintenance/populateChangeTagDefTest.php [new file with mode: 0644]
tests/phpunit/mocks/search/MockCompletionSearchEngine.php [new file with mode: 0644]
tests/phpunit/mocks/search/MockSearchEngine.php [new file with mode: 0644]
tests/phpunit/mocks/search/MockSearchResult.php [new file with mode: 0644]
tests/phpunit/mocks/search/MockSearchResultSet.php [new file with mode: 0644]
tests/phpunit/structure/AvailableRightsTest.php
tests/phpunit/structure/ContentHandlerSanityTest.php
tests/phpunit/structure/ExtensionJsonValidationTest.php
tests/phpunit/structure/ResourcesTest.php
tests/phpunit/structure/StructureTest.php
tests/phpunit/suites/ExtensionsTestSuite.php
tests/selenium/pageobjects/recentchanges.page.js [new file with mode: 0644]
tests/selenium/selenium.sh
tests/selenium/specs/page.js
tests/selenium/specs/specialrecentchanges.js [new file with mode: 0644]

diff --git a/HISTORY b/HISTORY
index 0693b21..7540af3 100644 (file)
--- a/HISTORY
+++ b/HISTORY
@@ -1,4 +1,490 @@
-Change notes from older releases. For current info see RELEASE-NOTES-1.31.
+Change notes from older releases. For current info see RELEASE-NOTES-1.32.
+
+= MediaWiki 1.31 =
+
+== MediaWiki 1.31.0 ==
+
+=== Changes since MediaWiki 1.31.0-rc.2 ===
+* (T195783) Initialize PSR-4 namespaces at same stage as normal autoloader.
+* (T196092) Hide MySQL binary/utf-8 charset option in the installer.
+* (T196185) Don't allow setting $wgDBmysql5 in the installer.
+* (T196125) php-memcached 3.0 (provided with PHP 7.0) is now supported.
+* (T182366) UploadBase::checkXMLEncodingMissmatch() now works on PHP 7.1+
+* (T118683) Fix exception from &$user deref on HHVM in the TitleMoveComplete hook.
+* (T196672) The mtime of extension.json files is now able to be zero
+* (T180403) Validate $length in padleft/padright parser functions.
+* (T143790) Make $wgEmailConfirmToEdit only affect edit actions.
+
+=== Changes since MediaWiki 1.31.0-rc.0 ===
+* (T33223) Drop archive.ar_text and ar_flags.
+* Add default edit rate limit of 90 edits/minute for all users.
+* (T187645) Use codepoint as tiebreaker when getting first-letters in
+  IcuCollation.
+* (T191947) Don't shell during the installer if shelling out is disabled.
+* (T194319) Improve duplicate config setting exception as part of extension
+  registration.
+* (T195211) Don't require trailing slash in PSR-4 autoloader directory.
+* (T186565) Fix PHP Notice from `ob_end_flush()` in `FileRepo::streamFile()`.
+* Do not incorrectly hide namespace input field in the installer.
+* (T186456) Refactor checks looking for PEAR maik libraries to be clearer.
+
+=== Important pre-upgrade notes for 1.31 ===
+* If you're using MySQL, SQLite, or MSSQL, are not using update.php to apply
+  schema changes, and cannot have downtime to run migrateArchiveText.php and
+  apply patch-drop-ar_text.sql manually, you'll have to apply a default value
+  to the ar_text and ar_flags columns of the archive table or make those
+  columns nullable before upgrading to MediaWiki 1.31.
+  maintenance/archives/patch-nullable-ar_text.sql shows how to do this for MySQL.
+
+=== Configuration changes in 1.31 ===
+* $wgEnableAPI and $wgEnableWriteAPI are now deprecated and will be removed in
+  a future version. The API is now considered to be stable, secure and
+  essential.
+* $wgUsejQueryThree was removed, as it is now the default. This was documented
+  as a temporary variable during the migration period, deprecated since 1.29.
+* $wgLogoHD has been updated to support svg images and uses $wgLogo where
+  possible for fallback images such as png.
+* (T44246) $wgFilterLogTypes will no longer ignore 'patrol' when user does not
+  have the right to mark things patrolled.
+* Wikis that contain imported revisions or CentralAuth global blocks should run
+  maintenance/cleanupUsersWithNoId.php.
+* The configuration settings $wgResourceLoaderMinifierStatementsOnOwnLine and
+  $wgResourceLoaderMinifierMaxLineLength, deprecated since 1.27, were removed.
+* (T180921) $wgReferrerPolicy now supports having fallbacks for browsers that
+  are not using the latest version of the Referrer Policy specification.
+* $wgFragmentMode is now set to [ 'legacy', 'html5' ] by default. This is a
+  first step of migration to human-readable section IDs that will later result
+  in 'html5' being the default mode.
+* CACHE_ACCEL now only supports APC(u) or WinCache. XCache support was removed
+  as upstream is inactive and has no plans to move to PHP 7.
+* The old CategorizedRecentChanges feature, including its related configuration
+  option $wgAllowCategorizedRecentChanges, has been removed.
+* (T188472) The 'comma' value for $wgArticleCountMethod is no longer supported
+  for performance reasons, and installations with this setting will now work as
+  if it was configured with 'any'.
+* (T185753) MediaWiki now defaults to using RemexHtml to tidy up user input,
+  rather than being off by default. If you wish to disable HTML tidying
+  entirely, set $wgTidyConfig to null; if you wish to use the old, deprecated
+  Tidy external binary, both set $wgTidyConfig to null and $wgUseTidy to true.
+* $wgLogAutopatrol now defaults to false instead of true.
+* $wgValidateAllHtml was removed and will be ignored.
+* $wgScriptExtension, deprecated and ignored since 1.25, was removed. See the
+  1.25 release notes for more information.
+* $wgUseAjax is now marked as deprecated, just like the deprecated AJAX
+  framework that it enables. Some extensions mistakenly used this to check
+  whether any AJAX functionality at all should be enabled, further making this
+  problematic to retain.
+* $wgDBmysql5 is now deprecated, and will be removed in a future version. It
+  has been marked as experimental ever since it was introduced.
+
+=== New features in 1.31 ===
+* (T76554) User sub-pages named ….json are now protected in the same way that
+  ….js and ….css pages are, so that configuration options can safely be placed
+  there.
+* Wikimedia\Rdbms\IDatabase->select() and similar methods now support joins
+  with parentheses for grouping.
+* As a first pass in standardizing dialog boxes across the MediaWiki product,
+  Html class now provides helper methods for messageBox, successBox, errorBox
+  and warningBox generation.
+* (T9240) Imports will now record unknown (and, optionally, known) usernames in
+  a format like "iw>Example".
+* (T20209) Linker (used on history pages, log pages, and so on) will display
+  usernames formed like "iw>Example" as interwiki links, as if by wikitext like
+  [[iw:User:Example|iw>Example]].
+* (T111605) The 'ImportHandleUnknownUser' hook allows extensions to auto-create
+  users during an import.
+* Added a hook, ParserOutputPostCacheTransform, to allow extensions to affect
+  the ParserOutput::getText() post-cache transformations.
+* Added a hook, UploadForm:getInitialPageText, to allow extensions to alter the
+  initial page text for file uploads.
+* (T181651) The info page for File pages now displays the file's base-16 SHA1
+  hash value in the table of basic information.
+* Style tags with a 'data-mw-deduplicate' attribute will be deduplicated as a
+  ParserOutput::getText() post-cache transformation. This may be disabled by
+  passing 'deduplicateStyles' => false to that method.
+* The identity of the logged-in or IP "actor" for logged actions is being moved
+  into a new actor table, with the rows in tables such as revision and logging
+  referring to the actor ID instead of storing the user ID and name/IP in
+  every row.
+  * This is currently gated by $wgActorTableSchemaMigrationStage. Most wikis
+    can set this to MIGRATION_NEW and run maintenance/migrateActors.php as
+    soon as any necessary extensions are updated.
+  * Most code accessing rows for logged actions from the database should use
+    the relevant getQueryInfo() methods to get the information needed to build
+    the SQL query. The ActorMigration class may also be used to get feature
+    -flagged information needed to access actor-related fields during the
+    migration period.
+* Added Wikimedia\Rdbms\IDatabase::cancelAtomic(), to roll back an atomic
+  section without having to roll back the whole transaction.
+* Wikimedia\Rdbms\IDatabase::doAtomicSection(), non-native ::insertSelect(),
+  and non-MySQL ::replace() and ::upsert() no longer roll back the whole
+  transaction on failure.
+* (T189785) Added a monthly heartbeat ping to the pingback feature.
+* The CLI installer (maintenance/install.php) learned to detect and include
+  extensions. Pass --with-extensions to enable that feature.
+* (T184791) rc_patrolled now has three states: "0" for unpatrolled,
+  "1" for manually patrolled and "2" for autopatrolled actions.
+* Extensions can now set their type to "editor" if they provide an editor or
+  enhance the editing experience.
+* Extensions can use a PSR-4 autoloader by setting an "AutoloadNamespaces"
+  property in extension.json. See the documentation at
+  <https://mediawiki.org/wiki/Manual:Extension.json/Schema#AutoloadNamespaces>
+  for more details and an example.
+* (T19099) Tabs which link to pages that don't exist (like those to uncreated
+  discussion pages) now have a tooltip to indicate state, not just colour.
+
+=== External library changes in 1.31 ===
+* pear/mail, pear/mail_mime and pear/mail_mime-decode have been moved from
+  suggested to required. These packages now must be installed via composer
+  and not via PEAR itself.
+
+==== Upgraded external libraries ====
+* Updated jquery.chosen from v0.9.14 to v1.8.2.
+* Updated composer/spdx-licenses from 1.1.4 to 1.3.0 (development dependency).
+* Updated nikic/php-parser from 2.1.0 to 3.1.3 (development dependency).
+* Updated wikimedia/ip-set from 1.1.0 to 1.2.0.
+* Updated wikimedia/relpath from 2.0.0 to 2.1.1.
+* Updated wikimedia/running-stat from 1.1.0 to 1.2.0.
+* Updated wikimedia/wrappedstring from 2.2.0 to 2.3.0.
+* Updated mediawiki/at-ease from 1.1.0 to 1.2.0.
+* Updated wikimedia/php-session-serializer from 1.0.4 to 1.0.6.
+* Updated wikimedia/remex-html from 1.0.2 to 1.0.3.
+* Updated wikimedia/html-formatter from 1.0.1 to 1.0.2.
+
+==== New external libraries ====
+* Added wikimedia/object-factory 1.0.0
+
+==== Removed and replaced external libraries ====
+* (T17845) The deprecated 'jquery.badge' module was removed.
+* The deprecated 'jquery.autoEllipsis' module was removed. Use the CSS
+  text-overflow property instead.
+* The deprecated 'jquery.placeholder' module was removed.
+* The deprecated 'jquery.appear' module was removed. Use the
+  'mediawiki.viewport' module instead.
+* mediawiki/at-ease was replaced with wikimedia/at-ease.
+
+=== Bug fixes in 1.31 ===
+* (T90902) Non-breaking space in header ID breaks anchor.
+* (T189375) CSSMin now allows quoted urls in `url()` syntax to start with a
+  space.
+* (T2087, T10897, T87753, T174639) Whitespace created by category and language
+  links is now stripped rather than leaving blank lines in odd places.
+* (T3780) Uploads with UTF-8 names now work on PHP7.1+ on Windows servers.
+* (T182366) UploadBase::checkXMLEncodingMissmatch() now works on PHP 7.1+
+
+=== Action API changes in 1.31 ===
+* (T185058) The 'name' value to tgprop for action=query&list=tags has been
+  removed. It has never made a difference in the output, the name was always
+  returned regardless.
+* The 'watch' and 'unwatch' parameters for action=move have been removed. They
+  were deprecated and also accidentally nonfunctional since 1.17 in 2010. Use
+  'watchlist' instead.
+
+=== Action API internal changes in 1.31 ===
+* ApiBase::getProfileDBTime, deprecated since 1.25, was removed.
+* ApiBase::getModuleProfileName, deprecated since 1.25, was removed.
+* ApiBase::getProfileTime, deprecated since 1.25, was removed.
+
+=== Languages updated in 1.31 ===
+MediaWiki supports over 350 languages. Many localisations are updated
+regularly. Below only new and removed languages are listed, as well as
+changes to languages because of Phabricator reports.
+
+* (T180052) Mirandese (mwl) now supports gendered NS_USER/NS_USER_TALK.
+* (T182305) New language support: Nyungar (nys).
+* (T186359) New language support: Siberian Tatar [cебертатар] (sty).
+* (T186635) New language support: Guianan Creole (gcr).
+* (T186647) New language support: Kumyk [къумукъ] (kum).
+* (T187750) New language support: Spanish formal address (es-formal).
+* (T187824) New language support: Hungarian formal address (hu-formal).
+* (T189127) New language support: Gorontalo (gor).
+
+=== Breaking changes in 1.31 ===
+* MessageBlobStore::insertMessageBlob(), deprecated in 1.27, was removed.
+* The OutputPage class constructor now requires a context parameter.
+  Instantiating without context was deprecated in 1.18.
+* The mw.page JavaScript singleton, deprecated in 1.30, was removed.
+* Article::getLastPurgeTimestamp(), WikiPage::getLastPurgeTimestamp(), and the
+  related WikiPage::PURGE_* constants, deprecated in 1.29, were removed.
+* The Article::selectFields(), ::onArticleCreate(), ::onArticleDelete(), and
+  ::onArticleEdit() methods, deprecated in 1.24, were removed.
+* Installer::locateExecutable() and ::locateExecutableInDefaultPaths() were
+  removed. Use ExecutableFinder::findInDefaultPaths() instead.
+* The deprecated MW_DIFF_VERSION constant was removed.
+  DifferenceEngine::MW_DIFF_VERSION should be used instead.
+* Due to significant refactoring, method ContribsPager::getUserCond() that had
+  no access restriction has been removed.
+* The Block class will no longer accept usable-but-missing usernames for
+  'byText' or ->setBlocker(). Callers should either ensure the blocker exists
+  locally or use a new interwiki-format username like "iw>Example".
+* The following methods and constants from the WatchedItem class, which were
+  deprecated in 1.27, have been removed:
+  * WatchedItem::getTitle()
+  * WatchedItem::fromUserTitle()
+  * WatchedItem::addWatch()
+  * WatchedItem::removeWatch()
+  * WatchedItem::isWatched()
+  * WatchedItem::duplicateEntries()
+  * WatchedItem::IGNORE_USER_RIGHTS
+  * WatchedItem::CHECK_USER_RIGHTS
+  * WatchedItem::DEPRECATED_USAGE_TIMESTAMP
+* The $statementsOnOwnLine parameter of JavaScriptMinifier::minify was removed.
+  $wgResourceLoaderMinifierStatementsOnOwnLine, the corresponding configuration
+  variable, has been deprecated since 1.27 and was removed as well.
+* The $maxLineLength parameter of JavaScriptMinifier::minify was removed.
+  $wgResourceLoaderMinifierMaxLineLength, the corresponding configuration
+  variable, has been deprecated since 1.27 and was removed as well.
+* The HtmlFormatter class, deprecated in 1.27, was removed. The namespaced
+  HtmlFormatter\HtmlFormatter class should be used instead.
+* The driver 'mysql' for MySQL, deprecated in MediaWiki 1.30, has been removed.
+  The driver has been deprecated since PHP 5.5 and was removed in PHP 7.0. The
+  default driver for MySQL has been 'mysqli' since MediaWiki 1.22.
+* The following properties of PreparedEdit were deprecated in 1.21 and have
+  been removed:
+  * PreparedEdit->newText
+  * PreparedEdit->oldText
+  * PreparedEdit->pst
+* ParserOutput objects which are generated using a non-default value for
+  ParserOptions::setWrapOutputClass() can no longer be added to the parser
+  cache.
+* The following deprecated methods from the OutputPage class have been removed:
+  * OutputPage::addExtensionStyle(); deprecated in 1.27
+  * OutputPage::getExtStyle(); deprecated in 1.27
+  * OutputPage::setETag(); deprecated in 1.28 (obsolete no-op)
+  * OutputPage::setSquidMaxage(); deprecated in 1.27
+  * OutputPage::readOnlyPage(); deprecated in 1.25
+  * OutputPage::rateLimited(); deprecated in 1.25
+  * Additionally, the protected OutputPage::$mExtStyles array, only accessed
+    through the above and with no known uses, was removed.
+* The no-op method Skin::showIPinHeader(), deprecated in 1.27, was removed.
+* The following variables and methods in EditPage, deprecated in MediaWiki 1.30,
+  were removed:
+  * $isCssJsSubpage — use ::isUserConfigPage()
+  * $isCssSubpage — use ::isUserCssConfigPage()
+  * $isJsSubpage — use ::isUserJsConfigPage()
+  * $isWrongCaseCssJsPage – use ::isWrongCaseUserConfigPage()
+  * ::getSummaryInput() – use ::getSummaryInputWidget()
+  * ::getSummaryInputOOUI() – use ::getSummaryInputWidget()
+  * ::getCheckboxes() – use ::getCheckboxesWidget() or
+      ::getCheckboxesDefinition()
+  * ::getCheckboxesOOUI() – use ::getCheckboxesWidget() or
+      ::getCheckboxesDefinition()
+* ResourceLoaderModule::getPosition(), deprecated in 1.29, has been removed.
+* In User, the cookie-related methods which were wrappers for the functions on
+  the response object, and were deprecated in 1.27, have been removed:
+  * ::setCookie()
+  * ::clearCookie()
+  * ::setExtendedLoginCookie()
+  Note that User::setCookies() remains, and is not deprecated.
+* Also in User, some auth-related methods which were deprecated in 1.27 have
+  been removed:
+  * ::getEditTokenTimestamp() – use MediaWiki\Session\Token::getTimestamp()
+  * ::getPasswordFactory() – create a PasswordFactory directly
+  * ::passwordChangeInputAttribs()
+* The global functions wfProfileIn and wfProfileOut, deprecated in 1.25, have
+  been removed.
+* SpecialPageFactory::getList(), deprecated in 1.24, has been removed. You can
+  use ::getNames() instead.
+* OpenSearch::getOpenSearchTemplate(), deprecated in 1.25, has been removed. You
+  can use ApiOpenSearch::getOpenSearchTemplate() instead.
+* The global function wfBaseConvert, deprecated in 1.27, has been removed. Use
+  Wikimedia\base_convert() directly.
+* Calling Database::begin() explicitly during an implicit transaction or when
+  DBO_TRX is set results in an exception. Calling Database::commit() explicitly
+  for an implicit transaction also results in an exception. Previously these
+  were logged as errors. The startAtomic() and endAtomic() methods, or
+  AtomicSectionUpdate should be used instead.
+* The global function wfOutputHandler() was removed, use the its replacement
+  MediaWiki\OutputHandler::handle() instead. The global function was only
+  sometimes defined. Its replacement is always available via the autoloader.
+* ChangeTags::listExtensionActivatedTags and ::listExtensionDefinedTags,
+  deprecated in 1.28, have been removed. Use ::listSoftwareActivatedTags() and
+  ::listSoftwareDefinedTags() instead.
+* Title::getTitleInvalidRegex(), deprecated in 1.25, has been removed. You can
+  use MediaWikiTitleCodec::getTitleInvalidRegex() instead.
+* HTMLForm & VFormHTMLForm::isVForm(), deprecated in 1.25, have been removed.
+* The ProfileSection class, deprecated in 1.25 and unused, has been removed.
+* The ResourceLoaderGetLessVars hook, deprecated in 1.30, has been removed. Use
+  ResourceLoaderModule::getLessVars() to expose local variables instead of
+  global ones.
+* As part of work to modernise user-generated content clean-up, a config option
+  and some methods related to HTML validity were removed without deprecation.
+  The public methods MWTidy::checkErrors() and the path through which it was
+  called, TidyDriverBase::validate(), are removed, as are the testing methods
+  MediaWikiTestCase::assertValidHtmlSnippet() and ::assertValidHtmlDocument().
+  The $wgValidateAllHtml configuration option is removed and will be ignored.
+* Execution of external programs using MediaWiki\Shell\Command now applies
+  the RESTRICT_DEFAULT Firejail restriction by default.
+* The ResourceLoaderModule::getHashMtime() and ::getDefinitionMtime() methods,
+  deprecated in 1.26, were removed.
+* The deprecated 'mediawiki.widgets.CategorySelector' module alias was removed.
+  Use the 'mediawiki.widgets.CategoryMultiselectWidget' module directly.
+
+=== Deprecations in 1.31 ===
+* The Revision class was deprecated in favor of RevisionStore, BlobStore, and
+  RevisionRecord and its subclasses.
+* The global function wfBCP47 is deprecated in favour of LanguageCode::bcp47.
+* The global function wfCountDown is now deprecated in favor of
+  Maintenance::countDown.
+* Several methods for returning lists of fields to select from the database
+  have been deprecated in favor of similar methods that also return the tables
+  to select from and the join conditions for those tables.
+  * Block::selectFields() → Block::getQueryInfo()
+  * RecentChange::selectFields() → RecentChange::getQueryInfo()
+  * ArchivedFile::selectFields() → ArchivedFile::getQueryInfo()
+  * LocalFile::selectFields() → LocalFile::getQueryInfo()
+  * LocalFile::getCacheFields() with a prefix no longer works
+  * LocalFile::getLazyCacheFields() with a prefix no longer works
+  * OldLocalFile::selectFields() → OldLocalFile::getQueryInfo()
+  * RecentChange::selectFields() → RecentChange::getQueryInfo()
+  * Revision::userJoinCond() → Revision::getQueryInfo( [ 'user' ] )
+  * Revision::selectUserFields() → Revision::getQueryInfo( [ 'user' ] )
+  * Revision::pageJoinCond() → Revision::getQueryInfo( [ 'page' ] )
+  * Revision::selectPageFields() → Revision::getQueryInfo( [ 'page' ] )
+  * Revision::selectTextFields() → Revision::getQueryInfo( [ 'text' ] )
+  * Revision::selectFields() → Revision::getQueryInfo()
+  * Revision::selectArchiveFields() → Revision::getArchiveQueryInfo()
+  * User::selectFields() → User::getQueryInfo()
+  * WikiPage::selectFields() → WikiPage::getQueryInfo()
+* Revision::setUserIdAndName() was deprecated.
+* Access to TitleValue class properties was deprecated, the relevant getters
+  should be used instead.
+* DifferenceEngine::getDiffBodyCacheKey() is deprecated. Subclasses should
+  override DifferenceEngine::getDiffBodyCacheKeyParams() instead.
+* Use of Maintenance::error( $err, $die ) to exit script was deprecated. Use
+  Maintenance::fatalError() instead.
+* Passing a ParserOptions object to OutputPage::parserOptions() is deprecated.
+* The RevisionInsertComplete hook is now deprecated; use instead the hook
+  RevisionRecordInserted. RevisionInsertComplete is still called, but the second
+  and third parameter will always be null. Hard deprecation is scheduled for 1.32.
+* The following methods that get and set ParserOutput state are deprecated.
+  Callers should use the new stateless $options parameter to
+  ParserOutput::getText() instead.
+  * ParserOptions::getEditSection()
+  * ParserOptions::setEditSection()
+  * ParserOutput::getEditSectionTokens()
+  * ParserOutput::setEditSectionTokens()
+  * ParserOutput::getTOCEnabled()
+  * ParserOutput::setTOCEnabled()
+  * OutputPage::enableSectionEditLinks()
+  * OutputPage::sectionEditLinksEnabled()
+  * The public ParserOutput state fields $mTOCEnabled and $mEditSectionTokens
+    are also deprecated.
+* License::getLicenses has been deprecated; use License::getLines instead.
+* QuickTemplate::setRef() was deprecated in favour of QuickTemplate::set().
+  Setting template variables by reference allowed violating the principle of
+  data being immutable once added to the skin template. In practice, this method
+  was not being used for that. Rather, setRef() existed as memory optimisation
+  for PHP 4.
+* QuickTemplate::setTranslator() and MediaWikiI18N::set() were deprecated in
+  favour of Skin::msg() parameters.
+* MediaWikiI18N::translate() was deprecated in favour of Skin::msg() or
+  wfMessage().
+* Passing false to ParserOptions::setWrapOutputClass() is deprecated. Use the
+  'unwrap' transform to ParserOutput::getText() instead.
+* \ObjectFactory (no namespace) is deprecated, the namespaced class
+  \Wikimedia\ObjectFactory from the wikimedia/object-factory library should be
+  used instead.
+* CommentStore::newKey is deprecated. Instead, get an instance from
+  MediaWikiServices.
+* The following CommentStore methods have had their signatures changed to
+  introduce a $key parameter, usage of the methods on instances retrieved from
+  CommentStore::newKey will remain unchanged but deprecated:
+  * CommentStore::getFields
+  * CommentStore::getJoin
+  * CommentStore::getComment
+  * CommentStore::getCommentLegacy
+  * CommentStore::insert
+  * CommentStore::insertWithTemplate
+* The following methods in Title have been renamed, and the old ones are
+  deprecated:
+  * Title::getSkinFromCssJsSubpage – use ::getSkinFromConfigSubpage
+  * Title::isCssOrJsPage – use ::isSiteConfigPage
+  * Title::isCssJsSubpage – use ::isUserConfigPage
+  * Title::isCssSubpage – use ::isUserCssConfigPage
+  * Title::isJsSubpage – use ::isUserJsConfigPage
+* The following methods related to caching of half-parsed HTML were deprecated:
+  * Parser::serializeHalfParsedText()
+  * Parser::unserializeHalfParsedText()
+  * Parser::isValidHalfParsedText()
+  * StripState::getSubState()
+  * StripState::merge()
+* The DeferredStringifier class is deprecated, use Message::listParam() instead.
+* The type string for the parameter $lang of DateFormatter::getInstance is
+  deprecated.
+* Wikimedia\Rdbms\SavepointPostgres is deprecated.
+* The DO_MAINTENANCE constant is deprecated. RUN_MAINTENANCE_IF_MAIN should be
+  used instead.
+* The function wfShellWikiCmd() has been deprecated, use
+  MediaWiki\Shell::makeScriptCommand().
+* In the future, the hooks 'PreferencesFormPreSave' and 'PreferencesGetLegend'
+  will be allowed to provide any HTMLForm object rather than PreferencesForm.
+
+=== Other changes in 1.31 ===
+* Browser support for Internet Explorer 10 was lowered from Grade A to Grade C.
+* Browser support for Opera 12 and older was dropped entirely. Opera 15+
+  continues at Grade A.
+* Multi-content-revision capability was introduced into the storage layer. See
+  <https://mediawiki.org/wiki/Requests_for_comment/Multi-Content_Revisions>.
+* The "free" CSS class is now only applied to unbracketed URLs in wikitext.
+  Links written using square brackets will get the class "text" not "free".
+* RFC 157418: Whitespace is trimmed from wikitext headings, wikitext list items,
+  wikitext table captions, wikitext table headings, wikitext table cells. HTML
+  headings, HTML list items, HTML table captions, HTML table headings, HTML
+  table cells will not have this trimming behavior.
+
+== Compatibility ==
+MediaWiki 1.31 requires PHP 7.0.0 or later. Although HHVM 3.18.5 or later is
+supported, it is generally advised to use PHP 7.0.0 or later for long term
+support.
+
+MySQL/MariaDB is the recommended DBMS. PostgreSQL or SQLite can also be used,
+but support for them is somewhat less mature. There is experimental support for
+Oracle and Microsoft SQL Server.
+
+The supported versions are:
+
+* MySQL 5.5.8 or later
+* PostgreSQL 9.2 or later
+* SQLite 3.3.7 or later
+* Oracle 9.0.1 or later
+* Microsoft SQL Server 2005 (9.00.1399)
+
+== Upgrading ==
+1.31 has several database changes since 1.30, and will not work without schema
+updates. Note that due to changes to some very large tables like the revision
+table, the schema update may take quite long (minutes on a medium sized site,
+many hours on a large site).
+
+Don't forget to always back up your database before upgrading!
+
+See the file UPGRADE for more detailed upgrade instructions, including
+important information when upgrading from versions prior to 1.11.
+
+For notes on 1.30.x and older releases, see HISTORY.
+
+== Online documentation ==
+Documentation for both end-users and site administrators is available on
+MediaWiki.org, and is covered under the GNU Free Documentation License (except
+for pages that explicitly state that their contents are in the public domain):
+
+       https://www.mediawiki.org/wiki/Special:MyLanguage/Documentation
+
+== Mailing list ==
+A mailing list is available for MediaWiki user support and discussion:
+
+       https://lists.wikimedia.org/mailman/listinfo/mediawiki-l
+
+A low-traffic announcements-only list is also available:
+
+       https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce
+
+It's highly recommended that you sign up for one of these lists if you're
+going to run a public MediaWiki, so you can be notified of security fixes.
+
+== IRC help ==
+There's usually someone online in #mediawiki on irc.freenode.net.
+
 
 = MediaWiki 1.30 =
 
diff --git a/RELEASE-NOTES-1.31 b/RELEASE-NOTES-1.31
deleted file mode 100644 (file)
index 8d5bab5..0000000
+++ /dev/null
@@ -1,487 +0,0 @@
-== MediaWiki 1.31 ==
-
-THIS IS NOT A RELEASE YET
-
-MediaWiki 1.31 is an alpha-quality branch and is not recommended for use in
-production.
-
-=== Changes since MediaWiki 1.31.0-rc.2 ===
-* (T195783) Initialize PSR-4 namespaces at same stage as normal autoloader.
-* (T196092) Hide MySQL binary/utf-8 charset option in the installer.
-* (T196185) Don't allow setting $wgDBmysql5 in the installer.
-* (T196125) php-memcached 3.0 (provided with PHP 7.0) is now supported.
-* (T182366) UploadBase::checkXMLEncodingMissmatch() now works on PHP 7.1+
-* (T118683) Fix exception from &$user deref on HHVM in the TitleMoveComplete hook.
-* (T196672) The mtime of extension.json files is now able to be zero
-* (T180403) Validate $length in padleft/padright parser functions.
-* (T143790) Make $wgEmailConfirmToEdit only affect edit actions.
-
-=== Changes since MediaWiki 1.31.0-rc.0 ===
-* (T33223) Drop archive.ar_text and ar_flags.
-* Add default edit rate limit of 90 edits/minute for all users.
-* (T187645) Use codepoint as tiebreaker when getting first-letters in
-  IcuCollation.
-* (T191947) Don't shell during the installer if shelling out is disabled.
-* (T194319) Improve duplicate config setting exception as part of extension
-  registration.
-* (T195211) Don't require trailing slash in PSR-4 autoloader directory.
-* (T186565) Fix PHP Notice from `ob_end_flush()` in `FileRepo::streamFile()`.
-* Do not incorrectly hide namespace input field in the installer.
-* (T186456) Refactor checks looking for PEAR maik libraries to be clearer.
-
-=== Important pre-upgrade notes for 1.31 ===
-* If you're using MySQL, SQLite, or MSSQL, are not using update.php to apply
-  schema changes, and cannot have downtime to run migrateArchiveText.php and
-  apply patch-drop-ar_text.sql manually, you'll have to apply a default value
-  to the ar_text and ar_flags columns of the archive table or make those
-  columns nullable before upgrading to MediaWiki 1.31.
-  maintenance/archives/patch-nullable-ar_text.sql shows how to do this for MySQL.
-
-=== Configuration changes in 1.31 ===
-* $wgEnableAPI and $wgEnableWriteAPI are now deprecated and will be removed in
-  a future version. The API is now considered to be stable, secure and
-  essential.
-* $wgUsejQueryThree was removed, as it is now the default. This was documented
-  as a temporary variable during the migration period, deprecated since 1.29.
-* $wgLogoHD has been updated to support svg images and uses $wgLogo where
-  possible for fallback images such as png.
-* (T44246) $wgFilterLogTypes will no longer ignore 'patrol' when user does not
-  have the right to mark things patrolled.
-* Wikis that contain imported revisions or CentralAuth global blocks should run
-  maintenance/cleanupUsersWithNoId.php.
-* The configuration settings $wgResourceLoaderMinifierStatementsOnOwnLine and
-  $wgResourceLoaderMinifierMaxLineLength, deprecated since 1.27, were removed.
-* (T180921) $wgReferrerPolicy now supports having fallbacks for browsers that
-  are not using the latest version of the Referrer Policy specification.
-* $wgFragmentMode is now set to [ 'legacy', 'html5' ] by default. This is a
-  first step of migration to human-readable section IDs that will later result
-  in 'html5' being the default mode.
-* CACHE_ACCEL now only supports APC(u) or WinCache. XCache support was removed
-  as upstream is inactive and has no plans to move to PHP 7.
-* The old CategorizedRecentChanges feature, including its related configuration
-  option $wgAllowCategorizedRecentChanges, has been removed.
-* (T188472) The 'comma' value for $wgArticleCountMethod is no longer supported
-  for performance reasons, and installations with this setting will now work as
-  if it was configured with 'any'.
-* (T185753) MediaWiki now defaults to using RemexHtml to tidy up user input,
-  rather than being off by default. If you wish to disable HTML tidying
-  entirely, set $wgTidyConfig to null; if you wish to use the old, deprecated
-  Tidy external binary, both set $wgTidyConfig to null and $wgUseTidy to true.
-* $wgLogAutopatrol now defaults to false instead of true.
-* $wgValidateAllHtml was removed and will be ignored.
-* $wgScriptExtension, deprecated and ignored since 1.25, was removed. See the
-  1.25 release notes for more information.
-* $wgUseAjax is now marked as deprecated, just like the deprecated AJAX
-  framework that it enables. Some extensions mistakenly used this to check
-  whether any AJAX functionality at all should be enabled, further making this
-  problematic to retain.
-* $wgDBmysql5 is now deprecated, and will be removed in a future version. It
-  has been marked as experimental ever since it was introduced.
-
-=== New features in 1.31 ===
-* (T76554) User sub-pages named ….json are now protected in the same way that
-  ….js and ….css pages are, so that configuration options can safely be placed
-  there.
-* Wikimedia\Rdbms\IDatabase->select() and similar methods now support joins
-  with parentheses for grouping.
-* As a first pass in standardizing dialog boxes across the MediaWiki product,
-  Html class now provides helper methods for messageBox, successBox, errorBox
-  and warningBox generation.
-* (T9240) Imports will now record unknown (and, optionally, known) usernames in
-  a format like "iw>Example".
-* (T20209) Linker (used on history pages, log pages, and so on) will display
-  usernames formed like "iw>Example" as interwiki links, as if by wikitext like
-  [[iw:User:Example|iw>Example]].
-* (T111605) The 'ImportHandleUnknownUser' hook allows extensions to auto-create
-  users during an import.
-* Added a hook, ParserOutputPostCacheTransform, to allow extensions to affect
-  the ParserOutput::getText() post-cache transformations.
-* Added a hook, UploadForm:getInitialPageText, to allow extensions to alter the
-  initial page text for file uploads.
-* (T181651) The info page for File pages now displays the file's base-16 SHA1
-  hash value in the table of basic information.
-* Style tags with a 'data-mw-deduplicate' attribute will be deduplicated as a
-  ParserOutput::getText() post-cache transformation. This may be disabled by
-  passing 'deduplicateStyles' => false to that method.
-* The identity of the logged-in or IP "actor" for logged actions is being moved
-  into a new actor table, with the rows in tables such as revision and logging
-  referring to the actor ID instead of storing the user ID and name/IP in
-  every row.
-  * This is currently gated by $wgActorTableSchemaMigrationStage. Most wikis
-    can set this to MIGRATION_NEW and run maintenance/migrateActors.php as
-    soon as any necessary extensions are updated.
-  * Most code accessing rows for logged actions from the database should use
-    the relevant getQueryInfo() methods to get the information needed to build
-    the SQL query. The ActorMigration class may also be used to get feature
-    -flagged information needed to access actor-related fields during the
-    migration period.
-* Added Wikimedia\Rdbms\IDatabase::cancelAtomic(), to roll back an atomic
-  section without having to roll back the whole transaction.
-* Wikimedia\Rdbms\IDatabase::doAtomicSection(), non-native ::insertSelect(),
-  and non-MySQL ::replace() and ::upsert() no longer roll back the whole
-  transaction on failure.
-* (T189785) Added a monthly heartbeat ping to the pingback feature.
-* The CLI installer (maintenance/install.php) learned to detect and include
-  extensions. Pass --with-extensions to enable that feature.
-* (T184791) rc_patrolled now has three states: "0" for unpatrolled,
-  "1" for manually patrolled and "2" for autopatrolled actions.
-* Extensions can now set their type to "editor" if they provide an editor or
-  enhance the editing experience.
-* Extensions can use a PSR-4 autoloader by setting an "AutoloadNamespaces"
-  property in extension.json. See the documentation at
-  <https://mediawiki.org/wiki/Manual:Extension.json/Schema#AutoloadNamespaces>
-  for more details and an example.
-* (T19099) Tabs which link to pages that don't exist (like those to uncreated
-  discussion pages) now have a tooltip to indicate state, not just colour.
-
-=== External library changes in 1.31 ===
-* pear/mail, pear/mail_mime and pear/mail_mime-decode have been moved from
-  suggested to required. These packages now must be installed via composer
-  and not via PEAR itself.
-
-==== Upgraded external libraries ====
-* Updated jquery.chosen from v0.9.14 to v1.8.2.
-* Updated composer/spdx-licenses from 1.1.4 to 1.3.0 (development dependency).
-* Updated nikic/php-parser from 2.1.0 to 3.1.3 (development dependency).
-* Updated wikimedia/ip-set from 1.1.0 to 1.2.0.
-* Updated wikimedia/relpath from 2.0.0 to 2.1.1.
-* Updated wikimedia/running-stat from 1.1.0 to 1.2.0.
-* Updated wikimedia/wrappedstring from 2.2.0 to 2.3.0.
-* Updated mediawiki/at-ease from 1.1.0 to 1.2.0.
-* Updated wikimedia/php-session-serializer from 1.0.4 to 1.0.6.
-* Updated wikimedia/remex-html from 1.0.2 to 1.0.3.
-* Updated wikimedia/html-formatter from 1.0.1 to 1.0.2.
-
-==== New external libraries ====
-* Added wikimedia/object-factory 1.0.0
-
-==== Removed and replaced external libraries ====
-* (T17845) The deprecated 'jquery.badge' module was removed.
-* The deprecated 'jquery.autoEllipsis' module was removed. Use the CSS
-  text-overflow property instead.
-* The deprecated 'jquery.placeholder' module was removed.
-* The deprecated 'jquery.appear' module was removed. Use the
-  'mediawiki.viewport' module instead.
-* mediawiki/at-ease was replaced with wikimedia/at-ease.
-
-=== Bug fixes in 1.31 ===
-* (T90902) Non-breaking space in header ID breaks anchor.
-* (T189375) CSSMin now allows quoted urls in `url()` syntax to start with a
-  space.
-* (T2087, T10897, T87753, T174639) Whitespace created by category and language
-  links is now stripped rather than leaving blank lines in odd places.
-* (T3780) Uploads with UTF-8 names now work on PHP7.1+ on Windows servers.
-* (T182366) UploadBase::checkXMLEncodingMissmatch() now works on PHP 7.1+
-
-=== Action API changes in 1.31 ===
-* (T185058) The 'name' value to tgprop for action=query&list=tags has been
-  removed. It has never made a difference in the output, the name was always
-  returned regardless.
-* The 'watch' and 'unwatch' parameters for action=move have been removed. They
-  were deprecated and also accidentally nonfunctional since 1.17 in 2010. Use
-  'watchlist' instead.
-
-=== Action API internal changes in 1.31 ===
-* ApiBase::getProfileDBTime, deprecated since 1.25, was removed.
-* ApiBase::getModuleProfileName, deprecated since 1.25, was removed.
-* ApiBase::getProfileTime, deprecated since 1.25, was removed.
-
-=== Languages updated in 1.31 ===
-MediaWiki supports over 350 languages. Many localisations are updated
-regularly. Below only new and removed languages are listed, as well as
-changes to languages because of Phabricator reports.
-
-* (T180052) Mirandese (mwl) now supports gendered NS_USER/NS_USER_TALK.
-* (T182305) New language support: Nyungar (nys).
-* (T186359) New language support: Siberian Tatar [cебертатар] (sty).
-* (T186635) New language support: Guianan Creole (gcr).
-* (T186647) New language support: Kumyk [къумукъ] (kum).
-* (T187750) New language support: Spanish formal address (es-formal).
-* (T187824) New language support: Hungarian formal address (hu-formal).
-* (T189127) New language support: Gorontalo (gor).
-
-=== Breaking changes in 1.31 ===
-* MessageBlobStore::insertMessageBlob(), deprecated in 1.27, was removed.
-* The OutputPage class constructor now requires a context parameter.
-  Instantiating without context was deprecated in 1.18.
-* The mw.page JavaScript singleton, deprecated in 1.30, was removed.
-* Article::getLastPurgeTimestamp(), WikiPage::getLastPurgeTimestamp(), and the
-  related WikiPage::PURGE_* constants, deprecated in 1.29, were removed.
-* The Article::selectFields(), ::onArticleCreate(), ::onArticleDelete(), and
-  ::onArticleEdit() methods, deprecated in 1.24, were removed.
-* Installer::locateExecutable() and ::locateExecutableInDefaultPaths() were
-  removed. Use ExecutableFinder::findInDefaultPaths() instead.
-* The deprecated MW_DIFF_VERSION constant was removed.
-  DifferenceEngine::MW_DIFF_VERSION should be used instead.
-* Due to significant refactoring, method ContribsPager::getUserCond() that had
-  no access restriction has been removed.
-* The Block class will no longer accept usable-but-missing usernames for
-  'byText' or ->setBlocker(). Callers should either ensure the blocker exists
-  locally or use a new interwiki-format username like "iw>Example".
-* The following methods and constants from the WatchedItem class, which were
-  deprecated in 1.27, have been removed:
-  * WatchedItem::getTitle()
-  * WatchedItem::fromUserTitle()
-  * WatchedItem::addWatch()
-  * WatchedItem::removeWatch()
-  * WatchedItem::isWatched()
-  * WatchedItem::duplicateEntries()
-  * WatchedItem::IGNORE_USER_RIGHTS
-  * WatchedItem::CHECK_USER_RIGHTS
-  * WatchedItem::DEPRECATED_USAGE_TIMESTAMP
-* The $statementsOnOwnLine parameter of JavaScriptMinifier::minify was removed.
-  $wgResourceLoaderMinifierStatementsOnOwnLine, the corresponding configuration
-  variable, has been deprecated since 1.27 and was removed as well.
-* The $maxLineLength parameter of JavaScriptMinifier::minify was removed.
-  $wgResourceLoaderMinifierMaxLineLength, the corresponding configuration
-  variable, has been deprecated since 1.27 and was removed as well.
-* The HtmlFormatter class, deprecated in 1.27, was removed. The namespaced
-  HtmlFormatter\HtmlFormatter class should be used instead.
-* The driver 'mysql' for MySQL, deprecated in MediaWiki 1.30, has been removed.
-  The driver has been deprecated since PHP 5.5 and was removed in PHP 7.0. The
-  default driver for MySQL has been 'mysqli' since MediaWiki 1.22.
-* The following properties of PreparedEdit were deprecated in 1.21 and have
-  been removed:
-  * PreparedEdit->newText
-  * PreparedEdit->oldText
-  * PreparedEdit->pst
-* ParserOutput objects which are generated using a non-default value for
-  ParserOptions::setWrapOutputClass() can no longer be added to the parser
-  cache.
-* The following deprecated methods from the OutputPage class have been removed:
-  * OutputPage::addExtensionStyle(); deprecated in 1.27
-  * OutputPage::getExtStyle(); deprecated in 1.27
-  * OutputPage::setETag(); deprecated in 1.28 (obsolete no-op)
-  * OutputPage::setSquidMaxage(); deprecated in 1.27
-  * OutputPage::readOnlyPage(); deprecated in 1.25
-  * OutputPage::rateLimited(); deprecated in 1.25
-  * Additionally, the protected OutputPage::$mExtStyles array, only accessed
-    through the above and with no known uses, was removed.
-* The no-op method Skin::showIPinHeader(), deprecated in 1.27, was removed.
-* The following variables and methods in EditPage, deprecated in MediaWiki 1.30,
-  were removed:
-  * $isCssJsSubpage — use ::isUserConfigPage()
-  * $isCssSubpage — use ::isUserCssConfigPage()
-  * $isJsSubpage — use ::isUserJsConfigPage()
-  * $isWrongCaseCssJsPage – use ::isWrongCaseUserConfigPage()
-  * ::getSummaryInput() – use ::getSummaryInputWidget()
-  * ::getSummaryInputOOUI() – use ::getSummaryInputWidget()
-  * ::getCheckboxes() – use ::getCheckboxesWidget() or
-      ::getCheckboxesDefinition()
-  * ::getCheckboxesOOUI() – use ::getCheckboxesWidget() or
-      ::getCheckboxesDefinition()
-* ResourceLoaderModule::getPosition(), deprecated in 1.29, has been removed.
-* In User, the cookie-related methods which were wrappers for the functions on
-  the response object, and were deprecated in 1.27, have been removed:
-  * ::setCookie()
-  * ::clearCookie()
-  * ::setExtendedLoginCookie()
-  Note that User::setCookies() remains, and is not deprecated.
-* Also in User, some auth-related methods which were deprecated in 1.27 have
-  been removed:
-  * ::getEditTokenTimestamp() – use MediaWiki\Session\Token::getTimestamp()
-  * ::getPasswordFactory() – create a PasswordFactory directly
-  * ::passwordChangeInputAttribs()
-* The global functions wfProfileIn and wfProfileOut, deprecated in 1.25, have
-  been removed.
-* SpecialPageFactory::getList(), deprecated in 1.24, has been removed. You can
-  use ::getNames() instead.
-* OpenSearch::getOpenSearchTemplate(), deprecated in 1.25, has been removed. You
-  can use ApiOpenSearch::getOpenSearchTemplate() instead.
-* The global function wfBaseConvert, deprecated in 1.27, has been removed. Use
-  Wikimedia\base_convert() directly.
-* Calling Database::begin() explicitly during an implicit transaction or when
-  DBO_TRX is set results in an exception. Calling Database::commit() explicitly
-  for an implicit transaction also results in an exception. Previously these
-  were logged as errors. The startAtomic() and endAtomic() methods, or
-  AtomicSectionUpdate should be used instead.
-* The global function wfOutputHandler() was removed, use the its replacement
-  MediaWiki\OutputHandler::handle() instead. The global function was only
-  sometimes defined. Its replacement is always available via the autoloader.
-* ChangeTags::listExtensionActivatedTags and ::listExtensionDefinedTags,
-  deprecated in 1.28, have been removed. Use ::listSoftwareActivatedTags() and
-  ::listSoftwareDefinedTags() instead.
-* Title::getTitleInvalidRegex(), deprecated in 1.25, has been removed. You can
-  use MediaWikiTitleCodec::getTitleInvalidRegex() instead.
-* HTMLForm & VFormHTMLForm::isVForm(), deprecated in 1.25, have been removed.
-* The ProfileSection class, deprecated in 1.25 and unused, has been removed.
-* The ResourceLoaderGetLessVars hook, deprecated in 1.30, has been removed. Use
-  ResourceLoaderModule::getLessVars() to expose local variables instead of
-  global ones.
-* As part of work to modernise user-generated content clean-up, a config option
-  and some methods related to HTML validity were removed without deprecation.
-  The public methods MWTidy::checkErrors() and the path through which it was
-  called, TidyDriverBase::validate(), are removed, as are the testing methods
-  MediaWikiTestCase::assertValidHtmlSnippet() and ::assertValidHtmlDocument().
-  The $wgValidateAllHtml configuration option is removed and will be ignored.
-* Execution of external programs using MediaWiki\Shell\Command now applies
-  the RESTRICT_DEFAULT Firejail restriction by default.
-* The ResourceLoaderModule::getHashMtime() and ::getDefinitionMtime() methods,
-  deprecated in 1.26, were removed.
-* The deprecated 'mediawiki.widgets.CategorySelector' module alias was removed.
-  Use the 'mediawiki.widgets.CategoryMultiselectWidget' module directly.
-
-=== Deprecations in 1.31 ===
-* The Revision class was deprecated in favor of RevisionStore, BlobStore, and
-  RevisionRecord and its subclasses.
-* The global function wfBCP47 is deprecated in favour of LanguageCode::bcp47.
-* The global function wfCountDown is now deprecated in favor of
-  Maintenance::countDown.
-* Several methods for returning lists of fields to select from the database
-  have been deprecated in favor of similar methods that also return the tables
-  to select from and the join conditions for those tables.
-  * Block::selectFields() → Block::getQueryInfo()
-  * RecentChange::selectFields() → RecentChange::getQueryInfo()
-  * ArchivedFile::selectFields() → ArchivedFile::getQueryInfo()
-  * LocalFile::selectFields() → LocalFile::getQueryInfo()
-  * LocalFile::getCacheFields() with a prefix no longer works
-  * LocalFile::getLazyCacheFields() with a prefix no longer works
-  * OldLocalFile::selectFields() → OldLocalFile::getQueryInfo()
-  * RecentChange::selectFields() → RecentChange::getQueryInfo()
-  * Revision::userJoinCond() → Revision::getQueryInfo( [ 'user' ] )
-  * Revision::selectUserFields() → Revision::getQueryInfo( [ 'user' ] )
-  * Revision::pageJoinCond() → Revision::getQueryInfo( [ 'page' ] )
-  * Revision::selectPageFields() → Revision::getQueryInfo( [ 'page' ] )
-  * Revision::selectTextFields() → Revision::getQueryInfo( [ 'text' ] )
-  * Revision::selectFields() → Revision::getQueryInfo()
-  * Revision::selectArchiveFields() → Revision::getArchiveQueryInfo()
-  * User::selectFields() → User::getQueryInfo()
-  * WikiPage::selectFields() → WikiPage::getQueryInfo()
-* Revision::setUserIdAndName() was deprecated.
-* Access to TitleValue class properties was deprecated, the relevant getters
-  should be used instead.
-* DifferenceEngine::getDiffBodyCacheKey() is deprecated. Subclasses should
-  override DifferenceEngine::getDiffBodyCacheKeyParams() instead.
-* Use of Maintenance::error( $err, $die ) to exit script was deprecated. Use
-  Maintenance::fatalError() instead.
-* Passing a ParserOptions object to OutputPage::parserOptions() is deprecated.
-* The RevisionInsertComplete hook is now deprecated; use instead the hook
-  RevisionRecordInserted. RevisionInsertComplete is still called, but the second
-  and third parameter will always be null. Hard deprecation is scheduled for 1.32.
-* The following methods that get and set ParserOutput state are deprecated.
-  Callers should use the new stateless $options parameter to
-  ParserOutput::getText() instead.
-  * ParserOptions::getEditSection()
-  * ParserOptions::setEditSection()
-  * ParserOutput::getEditSectionTokens()
-  * ParserOutput::setEditSectionTokens()
-  * ParserOutput::getTOCEnabled()
-  * ParserOutput::setTOCEnabled()
-  * OutputPage::enableSectionEditLinks()
-  * OutputPage::sectionEditLinksEnabled()
-  * The public ParserOutput state fields $mTOCEnabled and $mEditSectionTokens
-    are also deprecated.
-* License::getLicenses has been deprecated; use License::getLines instead.
-* QuickTemplate::setRef() was deprecated in favour of QuickTemplate::set().
-  Setting template variables by reference allowed violating the principle of
-  data being immutable once added to the skin template. In practice, this method
-  was not being used for that. Rather, setRef() existed as memory optimisation
-  for PHP 4.
-* QuickTemplate::setTranslator() and MediaWikiI18N::set() were deprecated in
-  favour of Skin::msg() parameters.
-* MediaWikiI18N::translate() was deprecated in favour of Skin::msg() or
-  wfMessage().
-* Passing false to ParserOptions::setWrapOutputClass() is deprecated. Use the
-  'unwrap' transform to ParserOutput::getText() instead.
-* \ObjectFactory (no namespace) is deprecated, the namespaced class
-  \Wikimedia\ObjectFactory from the wikimedia/object-factory library should be
-  used instead.
-* CommentStore::newKey is deprecated. Instead, get an instance from
-  MediaWikiServices.
-* The following CommentStore methods have had their signatures changed to
-  introduce a $key parameter, usage of the methods on instances retrieved from
-  CommentStore::newKey will remain unchanged but deprecated:
-  * CommentStore::getFields
-  * CommentStore::getJoin
-  * CommentStore::getComment
-  * CommentStore::getCommentLegacy
-  * CommentStore::insert
-  * CommentStore::insertWithTemplate
-* The following methods in Title have been renamed, and the old ones are
-  deprecated:
-  * Title::getSkinFromCssJsSubpage – use ::getSkinFromConfigSubpage
-  * Title::isCssOrJsPage – use ::isSiteConfigPage
-  * Title::isCssJsSubpage – use ::isUserConfigPage
-  * Title::isCssSubpage – use ::isUserCssConfigPage
-  * Title::isJsSubpage – use ::isUserJsConfigPage
-* The following methods related to caching of half-parsed HTML were deprecated:
-  * Parser::serializeHalfParsedText()
-  * Parser::unserializeHalfParsedText()
-  * Parser::isValidHalfParsedText()
-  * StripState::getSubState()
-  * StripState::merge()
-* The DeferredStringifier class is deprecated, use Message::listParam() instead.
-* The type string for the parameter $lang of DateFormatter::getInstance is
-  deprecated.
-* Wikimedia\Rdbms\SavepointPostgres is deprecated.
-* The DO_MAINTENANCE constant is deprecated. RUN_MAINTENANCE_IF_MAIN should be
-  used instead.
-* The function wfShellWikiCmd() has been deprecated, use
-  MediaWiki\Shell::makeScriptCommand().
-* In the future, the hooks 'PreferencesFormPreSave' and 'PreferencesGetLegend'
-  will be allowed to provide any HTMLForm object rather than PreferencesForm.
-
-=== Other changes in 1.31 ===
-* Browser support for Internet Explorer 10 was lowered from Grade A to Grade C.
-* Browser support for Opera 12 and older was dropped entirely. Opera 15+
-  continues at Grade A.
-* Multi-content-revision capability was introduced into the storage layer. See
-  <https://mediawiki.org/wiki/Requests_for_comment/Multi-Content_Revisions>.
-* The "free" CSS class is now only applied to unbracketed URLs in wikitext.
-  Links written using square brackets will get the class "text" not "free".
-* RFC 157418: Whitespace is trimmed from wikitext headings, wikitext list items,
-  wikitext table captions, wikitext table headings, wikitext table cells. HTML
-  headings, HTML list items, HTML table captions, HTML table headings, HTML
-  table cells will not have this trimming behavior.
-
-== Compatibility ==
-MediaWiki 1.31 requires PHP 7.0.0 or later. Although HHVM 3.18.5 or later is
-supported, it is generally advised to use PHP 7.0.0 or later for long term
-support.
-
-MySQL/MariaDB is the recommended DBMS. PostgreSQL or SQLite can also be used,
-but support for them is somewhat less mature. There is experimental support for
-Oracle and Microsoft SQL Server.
-
-The supported versions are:
-
-* MySQL 5.5.8 or later
-* PostgreSQL 9.2 or later
-* SQLite 3.3.7 or later
-* Oracle 9.0.1 or later
-* Microsoft SQL Server 2005 (9.00.1399)
-
-== Upgrading ==
-1.31 has several database changes since 1.30, and will not work without schema
-updates. Note that due to changes to some very large tables like the revision
-table, the schema update may take quite long (minutes on a medium sized site,
-many hours on a large site).
-
-Don't forget to always back up your database before upgrading!
-
-See the file UPGRADE for more detailed upgrade instructions, including
-important information when upgrading from versions prior to 1.11.
-
-For notes on 1.30.x and older releases, see HISTORY.
-
-== Online documentation ==
-Documentation for both end-users and site administrators is available on
-MediaWiki.org, and is covered under the GNU Free Documentation License (except
-for pages that explicitly state that their contents are in the public domain):
-
-       https://www.mediawiki.org/wiki/Special:MyLanguage/Documentation
-
-== Mailing list ==
-A mailing list is available for MediaWiki user support and discussion:
-
-       https://lists.wikimedia.org/mailman/listinfo/mediawiki-l
-
-A low-traffic announcements-only list is also available:
-
-       https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce
-
-It's highly recommended that you sign up for one of these lists if you're
-going to run a public MediaWiki, so you can be notified of security fixes.
-
-== IRC help ==
-There's usually someone online in #mediawiki on irc.freenode.net.
index b1f93f9..731f874 100644 (file)
@@ -42,6 +42,9 @@ production.
   reauthenticating.
 * FormSpecialPage::execute() will now call checkLoginSecurityLevel() if
   getLoginSecurityLevel() returns non-false.
+* The 'ImageBeforeProduceHTML' hook is now passed three new parameters, $parser,
+  &$query and &$widthOption, allowing extensions even finer control over the resulting
+  HTML code.
 
 === External library changes in 1.32 ===
 * …
@@ -160,6 +163,8 @@ because of Phabricator reports.
   or refer to T195256 for details on how to make the same change.
 * Hook 'EditPageBeforeEditChecks' was removed;
   use 'EditPageGetCheckboxesDefinition' instead.
+* Linker::getLinkColour() and DummyLinker::getLinkColour(), deprecated since
+  1.28, were removed. LinkRenderer::getLinkClasses() should be used instead.
 
 === Deprecations in 1.32 ===
 * Use of a StartProfiler.php file is deprecated in favour of placing
@@ -190,9 +195,25 @@ because of Phabricator reports.
 * The ApiQueryContributions class has been renamed to ApiQueryUserContribs.
 * The XMPInfo, XMPReader, and XMPValidate classes have been deprecated in favor
   of the namespaced classes provided by the wikimedia/xmp-reader library.
+* SearchResultSet::{next,rewind} are deprecated. Calling code should
+  use foreach on the SearchResultSet, or the extractResults method. Extending
+  code should override extractResults.
+* Instantiating SearchResultSet directly is deprecated. SearchEngine
+  implementations must subclass SearchResultSet for their purposes.
+* SearchResult::setExtensionData argument has been changed from accepting an
+  array to accepting a Closure that returns the array when called.
 * Class CryptRand, everything in MWCryptRand except generateHex() and function
   MediaWikiServices::getCryptRand() are deprecated, use random_bytes() to
   generate cryptographically secure random byte sequences.
+* Parser::getConverterLanguage() is deprecated.  Use ::getTargetLanguage()
+  instead.
+* Language::markNoConversion() is deprecated.  It confused readers because
+  it had unexpected behavior (only marking text if it looked like a URL)
+  and was only used in a single place in the code.  Use
+  LanguageConverter::markNoConversion() instead.
+* (T176526) EditPage::getContextTitle() falling back to $wgTitle when the
+  context title is unset is now deprecated; anything creating an EditPage
+  instance should set the context title via ::setContextTitle().
 
 === Other changes in 1.32 ===
 * …
index 1adc5e4..6b1f981 100644 (file)
@@ -1070,6 +1070,7 @@ $wgAutoloadLocalClasses = [
        'PageProps' => __DIR__ . '/includes/PageProps.php',
        'PageQueryPage' => __DIR__ . '/includes/specialpage/PageQueryPage.php',
        'Pager' => __DIR__ . '/includes/pager/Pager.php',
+       'PaginatingSearchEngine' => __DIR__ . '/includes/search/PaginatingSearchEngine.php',
        'ParameterizedPassword' => __DIR__ . '/includes/password/ParameterizedPassword.php',
        'Parser' => __DIR__ . '/includes/parser/Parser.php',
        'ParserCache' => __DIR__ . '/includes/parser/ParserCache.php',
@@ -1102,6 +1103,7 @@ $wgAutoloadLocalClasses = [
        'PopulateArchiveRevId' => __DIR__ . '/maintenance/populateArchiveRevId.php',
        'PopulateBacklinkNamespace' => __DIR__ . '/maintenance/populateBacklinkNamespace.php',
        'PopulateCategory' => __DIR__ . '/maintenance/populateCategory.php',
+       'PopulateChangeTagDef' => __DIR__ . '/maintenance/populateChangeTagDef.php',
        'PopulateContentModel' => __DIR__ . '/maintenance/populateContentModel.php',
        'PopulateExternallinksIndex60' => __DIR__ . '/maintenance/populateExternallinksIndex60.php',
        'PopulateFilearchiveSha1' => __DIR__ . '/maintenance/populateFilearchiveSha1.php',
index 99a3f64..7eb4265 100644 (file)
@@ -65,7 +65,7 @@
                "nikic/php-parser": "3.1.3",
                "nmred/kafka-php": "0.1.5",
                "phpunit/phpunit": "4.8.36 || ^6.5",
-               "psy/psysh": "0.8.11",
+               "psy/psysh": "0.9.6",
                "wikimedia/avro": "1.8.0",
                "wikimedia/testing-access-wrapper": "~1.0",
                "wmde/hamcrest-html-matchers": "^0.1.0"
index 520133d..b452b94 100644 (file)
@@ -74,10 +74,10 @@ Using a hook-running strategy, we can avoid having all this option-specific
 stuff in our mainline code. Using hooks, the function becomes:
 
        function showAnArticle( $article ) {
-               if ( Hooks::run( 'ArticleShow', array( &$article ) ) ) {
+               if ( Hooks::run( 'ArticleShow', [ &$article ] ) ) {
                        # code to actually show the article goes here
 
-                       Hooks::run( 'ArticleShowComplete', array( &$article ) );
+                       Hooks::run( 'ArticleShowComplete', [ &$article ] );
                }
        }
 
@@ -137,13 +137,13 @@ Hooks are registered by adding them to the global $wgHooks array for a given
 event. All the following are valid ways to define hooks:
 
        $wgHooks['EventName'][] = 'someFunction'; # function, no data
-       $wgHooks['EventName'][] = array( 'someFunction', $someData );
-       $wgHooks['EventName'][] = array( 'someFunction' ); # weird, but OK
+       $wgHooks['EventName'][] = [ 'someFunction', $someData ];
+       $wgHooks['EventName'][] = [ 'someFunction' ]; # weird, but OK
 
        $wgHooks['EventName'][] = $object; # object only
-       $wgHooks['EventName'][] = array( $object, 'someMethod' );
-       $wgHooks['EventName'][] = array( $object, 'someMethod', $someData );
-       $wgHooks['EventName'][] = array( $object ); # weird but OK
+       $wgHooks['EventName'][] = [ $object, 'someMethod' ];
+       $wgHooks['EventName'][] = [ $object, 'someMethod', $someData ];
+       $wgHooks['EventName'][] = [ $object ]; # weird but OK
 
 When an event occurs, the function (or object method) will be called with the
 optional data provided as well as event-specific parameters. The above examples
@@ -168,8 +168,8 @@ different: 'onArticleSave', 'onUserLogin', etc.
 The extra data is useful if we want to use the same function or object for
 different purposes. For example:
 
-       $wgHooks['PageContentSaveComplete'][] = array( 'ircNotify', 'TimStarling' );
-       $wgHooks['PageContentSaveComplete'][] = array( 'ircNotify', 'brion' );
+       $wgHooks['PageContentSaveComplete'][] = [ 'ircNotify', 'TimStarling' ];
+       $wgHooks['PageContentSaveComplete'][] = [ 'ircNotify', 'brion' ];
 
 This code would result in ircNotify being run twice when an article is saved:
 once for 'TimStarling', and once for 'brion'.
@@ -187,7 +187,7 @@ The last result would be for cases where the hook function replaces the main
 functionality. For example, if you wanted to authenticate users to a custom
 system (LDAP, another PHP program, whatever), you could do:
 
-       $wgHooks['UserLogin'][] = array( 'ldapLogin', $ldapServer );
+       $wgHooks['UserLogin'][] = [ 'ldapLogin', $ldapServer ];
 
        function ldapLogin( $username, $password ) {
                # log user into LDAP
@@ -232,7 +232,7 @@ wfRunHooks must be used, which was deprecated in MediaWiki 1.25.
 Note that hook parameters are passed in an array; this is a necessary
 inconvenience to make it possible to pass reference values (that can be changed)
 into the hook code. Also note that earlier versions of wfRunHooks took a
-variable number of arguments; the array() calling protocol came about after
+variable number of arguments; the array calling protocol came about after
 MediaWiki 1.4rc1.
 
 ==Events and parameters==
@@ -240,8 +240,8 @@ MediaWiki 1.4rc1.
 This is a list of known events and parameters; please add to it if you're going
 to add events to the MediaWiki code.
 
-'AbortAutoAccount': DEPRECATED! Create a PreAuthenticationProvider instead.
-Return false to cancel automated local account creation, where normally
+'AbortAutoAccount': DEPRECATED since 1.27! Create a PreAuthenticationProvider
+instead. Return false to cancel automated local account creation, where normally
 authentication against an external auth plugin would be creating a local
 account.
 $user: the User object about to be created (read-only, incomplete)
@@ -259,7 +259,7 @@ $editor: The User who made the change.
 $title: The Title of the page that was edited.
 $rc: The current RecentChange object.
 
-'AbortLogin': DEPRECATED! Create a PreAuthenticationProvider instead.
+'AbortLogin': DEPRECATED since 1.27! Create a PreAuthenticationProvider instead.
 Return false to cancel account login.
 $user: the User object being authenticated against
 $password: the password being submitted, not yet checked for validity
@@ -269,8 +269,8 @@ $password: the password being submitted, not yet checked for validity
 &$msg: the message identifier for abort reason (new in 1.18, not available
   before 1.18)
 
-'AbortNewAccount': DEPRECATED! Create a PreAuthenticationProvider instead.
-Return false to cancel explicit account creation.
+'AbortNewAccount': DEPRECATED since 1.27! Create a PreAuthenticationProvider
+instead. Return false to cancel explicit account creation.
 $user: the User object about to be created (read-only, incomplete)
 &$msg: out parameter: HTML to display on abort
 &$status: out parameter: Status object to return, replaces the older $msg param
@@ -294,14 +294,14 @@ $name: name of the action
 &$fields: HTMLForm descriptor array
 $article: Article object
 
-'AddNewAccount': DEPRECATED! Use LocalUserCreated.
+'AddNewAccount': DEPRECATED since 1.27! Use LocalUserCreated.
 After a user account is created.
 $user: the User object that was created. (Parameter added in 1.7)
 $byEmail: true when account was created "by email" (added in 1.12)
 
-'AfterBuildFeedLinks': Executed in OutputPage.php after all feed links (atom, rss,...)
-are created. Can be used to omit specific feeds from being outputted. You must not use
-this hook to add feeds, use OutputPage::addFeedLink() instead.
+'AfterBuildFeedLinks': Executed in OutputPage.php after all feed links (atom,
+rss,...) are created. Can be used to omit specific feeds from being outputted.
+You must not use this hook to add feeds, use OutputPage::addFeedLink() instead.
 &$feedLinks: Array of created feed links
 
 'AfterFinalPageOutput': Nearly at the end of OutputPage::output() but
@@ -372,7 +372,7 @@ from ApiBase::addDeprecation().
 &$msgs: Message[] Messages to include in the help. Multiple messages will be
   joined with spaces.
 
-'APIEditBeforeSave': DEPRECATED! Use EditFilterMergedContent instead.
+'APIEditBeforeSave': DEPRECATED since 1.28! Use EditFilterMergedContent instead.
 Before saving a page with api.php?action=edit, after
 processing request parameters. Return false to let the request fail, returning
 an error message or an <edit result="Failure"> tag if $resultArr was filled.
@@ -409,8 +409,8 @@ $format: API format code for $text.
 &$params: Array of parameters
 $flags: int zero or OR-ed flags like ApiBase::GET_VALUES_FOR_HELP
 
-'APIGetDescription': DEPRECATED! Use APIGetDescriptionMessages instead.
-Use this hook to modify a module's description.
+'APIGetDescription': DEPRECATED since 1.25! Use APIGetDescriptionMessages
+instead. Use this hook to modify a module's description.
 &$module: ApiBase Module object
 &$desc: String description, or array of description strings
 
@@ -418,8 +418,8 @@ Use this hook to modify a module's description.
 $module: ApiBase Module object
 &$msg: Array of Message objects
 
-'APIGetParamDescription': DEPRECATED! Use APIGetParamDescriptionMessages
-instead.
+'APIGetParamDescription': DEPRECATED since 1.25! Use
+APIGetParamDescriptionMessages instead.
 Use this hook to modify a module's parameter descriptions.
 &$module: ApiBase Module object
 &$desc: Array of parameter descriptions
@@ -488,8 +488,8 @@ documentation.
 $module: ApiQueryBase module in question
 $result: ResultWrapper|bool returned from the IDatabase::select()
 &$hookData: array that was passed to the 'ApiQueryBaseBeforeQuery' hook and
- will be passed to the 'ApiQueryBaseProcessRow' hook, intended for inter-hook
- communication.
 will be passed to the 'ApiQueryBaseProcessRow' hook, intended for inter-hook
 communication.
 
 'ApiQueryBaseBeforeQuery': Called for (some) API query modules before a
 database query is made. WARNING: It would be very easy to misuse this hook and
@@ -504,7 +504,7 @@ $module: ApiQueryBase module in question
 &$query_options: array of options for the database request
 &$join_conds: join conditions for the tables
 &$hookData: array that will be passed to the 'ApiQueryBaseAfterQuery' and
- 'ApiQueryBaseProcessRow' hooks, intended for inter-hook communication.
 'ApiQueryBaseProcessRow' hooks, intended for inter-hook communication.
 
 'ApiQueryBaseProcessRow': Called for (some) API query modules as each row of
 the database result is processed. Return false to stop processing the result
@@ -514,26 +514,26 @@ $module: ApiQueryBase module in question
 $row: stdClass Database result row
 &$data: array to be included in the ApiResult.
 &$hookData: array that was be passed to the 'ApiQueryBaseBeforeQuery' and
- 'ApiQueryBaseAfterQuery' hooks, intended for inter-hook communication.
 'ApiQueryBaseAfterQuery' hooks, intended for inter-hook communication.
 
 'APIQueryGeneratorAfterExecute': After calling the executeGenerator() method of
 an action=query submodule. Use this to extend core API modules.
 &$module: Module object
 &$resultPageSet: ApiPageSet object
 
-'APIQueryInfoTokens': DEPRECATED! Use ApiQueryTokensRegisterTypes instead.
-Use this hook to add custom tokens to prop=info. Every token has an action,
-which will be used in the intoken parameter and in the output
+'APIQueryInfoTokens': DEPRECATED since 1.24! Use ApiQueryTokensRegisterTypes
+instead. Use this hook to add custom tokens to prop=info. Every token has an
+action, which will be used in the intoken parameter and in the output
 (actiontoken="..."), and a callback function which should return the token, or
 false if the user isn't allowed to obtain it. The prototype of the callback
 function is func($pageid, $title), where $pageid is the page ID of the page the
 token is requested for and $title is the associated Title object. In the hook,
 just add your callback to the $tokenFunctions array and return true (returning
 false makes no sense).
-&$tokenFunctions: array(action => callback)
+&$tokenFunctions: [ action => callback ]
 
-'APIQueryRecentChangesTokens': DEPRECATED! Use ApiQueryTokensRegisterTypes
-instead.
+'APIQueryRecentChangesTokens': DEPRECATED since 1.24! Use
+ApiQueryTokensRegisterTypes instead.
 Use this hook to add custom tokens to list=recentchanges. Every token has an
 action, which will be used in the rctoken parameter and in the output
 (actiontoken="..."), and a callback function which should return the token, or
@@ -543,9 +543,10 @@ page associated to the revision the token is requested for, $title the
 associated Title object and $rc the associated RecentChange object. In the
 hook, just add your callback to the $tokenFunctions array and return true
 (returning false makes no sense).
-&$tokenFunctions: array(action => callback)
+&$tokenFunctions: [ action => callback ]
 
-'APIQueryRevisionsTokens': DEPRECATED! Use ApiQueryTokensRegisterTypes instead.
+'APIQueryRevisionsTokens': DEPRECATED since 1.24! Use
+ApiQueryTokensRegisterTypes instead.
 Use this hook to add custom tokens to prop=revisions. Every token has an
 action, which will be used in the rvtoken parameter and in the output
 (actiontoken="..."), and a callback function which should return the token, or
@@ -555,7 +556,7 @@ page associated to the revision the token is requested for, $title the
 associated Title object and $rev the associated Revision object. In the hook,
 just add your callback to the $tokenFunctions array and return true (returning
 false makes no sense).
-&$tokenFunctions: array(action => callback)
+&$tokenFunctions: [ action => callback ]
 
 'APIQuerySiteInfoGeneralInfo': Use this hook to add extra information to the
 sites general information.
@@ -569,10 +570,11 @@ sites statistics information.
 'ApiQueryTokensRegisterTypes': Use this hook to add additional token types to
 action=query&meta=tokens. Note that most modules will probably be able to use
 the 'csrf' token instead of creating their own token types.
-&$salts: array( type => salt to pass to User::getEditToken() or array of salt
-  and key to pass to Session::getToken() )
+&$salts: [ type => salt to pass to User::getEditToken(), or array of salt
+  and key to pass to Session::getToken() ]
 
-'APIQueryUsersTokens': DEPRECATED! Use ApiQueryTokensRegisterTypes instead.
+'APIQueryUsersTokens': DEPRECATED since 1.24! Use ApiQueryTokensRegisterTypes
+instead.
 Use this hook to add custom token to list=users. Every token has an action,
 which will be used in the ustoken parameter and in the output
 (actiontoken="..."), and a callback function which should return the token, or
@@ -580,7 +582,7 @@ false if the user isn't allowed to obtain it. The prototype of the callback
 function is func($user) where $user is the User object. In the hook, just add
 your callback to the $tokenFunctions array and return true (returning false
 makes no sense).
-&$tokenFunctions: array(action => callback)
+&$tokenFunctions: [ action => callback ]
 
 'ApiQueryWatchlistExtractOutputData': Extract row data for ApiQueryWatchlist.
 $module: ApiQueryWatchlist instance
@@ -600,7 +602,7 @@ key for the array that represents the service data. In this data array, the
 key-value-pair identified by the apiLink key is required.
 &$apis: array of services
 
-'ApiTokensGetTokenTypes': DEPRECATED! Use ApiQueryTokensRegisterTypes instead.
+'ApiTokensGetTokenTypes': DEPRECATED since 1.24! Use ApiQueryTokensRegisterTypes instead.
 Use this hook to extend action=tokens with new token types.
 &$tokenTypes: supported token types in format 'type' => callback function
   used to retrieve this type of tokens.
@@ -778,7 +780,8 @@ redirect was followed.
 from a set of AuthenticationRequest classes into a form descriptor; hooks
 can tweak the array to change how login etc. forms should look.
 $requests: array of AuthenticationRequests the fields are created from
-$fieldInfo: field information array (union of all AuthenticationRequest::getFieldInfo() responses).
+$fieldInfo: field information array (union of all
+  AuthenticationRequest::getFieldInfo() responses).
 &$formDescriptor: HTMLForm descriptor. The special key 'weight' can be set
   to change the order of the fields.
 $action: one of the AuthManager::ACTION_* constants.
@@ -786,20 +789,21 @@ $action: one of the AuthManager::ACTION_* constants.
 'AuthManagerLoginAuthenticateAudit': A login attempt either succeeded or failed
 for a reason other than misconfiguration or session loss. No return data is
 accepted; this hook is for auditing only.
-$response: The MediaWiki\Auth\AuthenticationResponse in either a PASS or FAIL state.
+$response: The MediaWiki\Auth\AuthenticationResponse in either a PASS or FAIL
+  state.
 $user: The User object being authenticated against, or null if authentication
   failed before getting that far.
 $username: A guess at the user name being authenticated, or null if we can't
   even determine that.
 
-'AuthPluginAutoCreate': DEPRECATED! Use the 'LocalUserCreated' hook instead.
-Called when creating a local account for an user logged in from an external
-authentication method.
+'AuthPluginAutoCreate': DEPRECATED since 1.27! Use the 'LocalUserCreated' hook
+instead. Called when creating a local account for an user logged in from an
+external authentication method.
 $user: User object created locally
 
-'AuthPluginSetup': DEPRECATED! Extensions should be updated to use AuthManager.
-Update or replace authentication plugin object ($wgAuth). Gives a chance for an
-extension to set it programmatically to a variable class.
+'AuthPluginSetup': DEPRECATED since 1.27! Extensions should be updated to use
+AuthManager. Update or replace authentication plugin object ($wgAuth). Gives a
+chance for an extension to set it programmatically to a variable class.
 &$auth: the $wgAuth object, probably a stub
 
 'AutopromoteCondition': Check autopromote condition for user.
@@ -972,9 +976,9 @@ No return data is accepted; this hook is for auditing only.
 $req: AuthenticationRequest object describing the change (and target user)
 $status: StatusValue with the result of the action
 
-'ChangePasswordForm': DEPRECATED! Use AuthChangeFormFields or security levels.
-For extensions that need to add a field to the ChangePassword form via the
-Preferences form.
+'ChangePasswordForm': DEPRECATED since 1.27! Use AuthChangeFormFields or
+security levels. For extensions that need to add a field to the ChangePassword
+form via the Preferences form.
 &$extraFields: An array of arrays that hold fields like would be passed to the
   pretty function.
 
@@ -991,8 +995,8 @@ $rows: The data that will be rendered. May be a ResultWrapper instance or
 $unpatrolled: Whether or not we are showing unpatrolled changes.
 $watched: Whether or not the change is watched by the user.
 
-'ChangesListSpecialPageFilters': DEPRECATED! Use 'ChangesListSpecialPageStructuredFilters'
-instead.
+'ChangesListSpecialPageFilters': DEPRECATED since 1.29! Use
+'ChangesListSpecialPageStructuredFilters' instead.
 Called after building form options on pages
 inheriting from ChangesListSpecialPage (in core: RecentChanges,
 RecentChangesLinked and Watchlist).
@@ -1075,8 +1079,9 @@ $rc_id: recentchanges table id
 $rev_id: revision table id
 $log_id: logging table id
 $params: tag params
-$rc: RecentChange being tagged when the tagging accompanies the action or null
-$user: User who performed the tagging when the tagging is subsequent to the action or null
+$rc: RecentChange being tagged when the tagging accompanies the action, or null
+$user: User who performed the tagging when the tagging is subsequent to the
+  action, or null
 
 'ChangeTagsAllowedAdd': Called when checking if a user can add tags to a change.
 &$allowedTags: List of all the tags the user is allowed to add. Any tags the
@@ -1186,16 +1191,16 @@ $lossy:   boolean indicating whether lossy conversion is allowed.
   converted Content object. Note that $result->getContentModel() must return
   $toModel.
 
-'ContentSecurityPolicyDefaultSource': Modify the allowed CSP load sources. This affects all
-directives except for the script directive. If you want to add a script
-source, see ContentSecurityPolicyScriptSource hook.
+'ContentSecurityPolicyDefaultSource': Modify the allowed CSP load sources. This
+affects all directives except for the script directive. If you want to add a
+script source, see ContentSecurityPolicyScriptSource hook.
 &$defaultSrc: Array of Content-Security-Policy allowed sources
 $policyConfig: Current configuration for the Content-Security-Policy header
-$mode: ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE
-  depending on type of header
+$mode: ContentSecurityPolicy::REPORT_ONLY_MODE or
+  ContentSecurityPolicy::FULL_MODE depending on type of header
 
-'ContentSecurityPolicyDirectives': Modify the content security policy directives.
-Use this only if ContentSecurityPolicyDefaultSource and
+'ContentSecurityPolicyDirectives': Modify the content security policy
+directives. Use this only if ContentSecurityPolicyDefaultSource and
 ContentSecurityPolicyScriptSource do not meet your needs.
 &$directives: Array of CSP directives
 $policyConfig: Current configuration for the CSP header
@@ -1208,8 +1213,8 @@ want non-script sources to be loaded from
 whatever you add.
 &$scriptSrc: Array of CSP directives
 $policyConfig: Current configuration for the CSP header
-$mode: ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE
-  depending on type of header
+$mode: ContentSecurityPolicy::REPORT_ONLY_MODE or
+  ContentSecurityPolicy::FULL_MODE depending on type of header
 
 'CustomEditor': When invoking the page editor
 Return true to allow the normal editor to be used, or false if implementing
@@ -1240,12 +1245,15 @@ $row: the DB row for this line
   Currently only data attributes reserved to MediaWiki are allowed
   (see Sanitizer::isReservedDataAttribute).
 
-'DeleteUnknownPreferences': Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which
-to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences
-that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed
-with 'gadget-', and so anything with that prefix is excluded from the deletion.
-&where: An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted
-  from the user_properties table.
+'DeleteUnknownPreferences': Called by the cleanupPreferences.php maintenance
+script to build a WHERE clause with which to delete preferences that are not
+known about. This hook is used by extensions that have dynamically-named
+preferences that should not be deleted in the usual cleanup process. For
+example, the Gadgets extension creates preferences prefixed with 'gadget-', and
+so anything with that prefix is excluded from the deletion.
+&where: An array that will be passed as the $cond parameter to
+  IDatabase::select() to determine what will be deleted from the user_properties
+  table.
 $db: The IDatabase object, useful for accessing $db->buildLike() etc.
 
 'DifferenceEngineAfterLoadNewText': called in DifferenceEngine::loadNewText()
@@ -1260,82 +1268,88 @@ checking if the variable's value is null.
 This hook can be used to inject content into said class member variable.
 $differenceEngine: DifferenceEngine object
 
-'DifferenceEngineMarkPatrolledLink': Allows extensions to change the "mark as patrolled" link
-which is shown both on the diff header as well as on the bottom of a page, usually
-wrapped in a span element which has class="patrollink".
+'DifferenceEngineMarkPatrolledLink': Allows extensions to change the "mark as
+patrolled" link which is shown both on the diff header as well as on the bottom
+of a page, usually wrapped in a span element which has class="patrollink".
 $differenceEngine: DifferenceEngine object
 &$markAsPatrolledLink: The "mark as patrolled" link HTML (string)
 $rcid: Recent change ID (rc_id) for this change (int)
 
-'DifferenceEngineMarkPatrolledRCID': Allows extensions to possibly change the rcid parameter.
-For example the rcid might be set to zero due to the user being the same as the
-performer of the change but an extension might still want to show it under certain
-conditions.
+'DifferenceEngineMarkPatrolledRCID': Allows extensions to possibly change the
+rcid parameter. For example the rcid might be set to zero due to the user being
+the same as the performer of the change but an extension might still want to
+show it under certain conditions.
 &$rcid: rc_id (int) of the change or 0
 $differenceEngine: DifferenceEngine object
 $change: RecentChange object
 $user: User object representing the current user
 
-'DifferenceEngineNewHeader': Allows extensions to change the $newHeader variable, which
-contains information about the new revision, such as the revision's author, whether
-the revision was marked as a minor edit or not, etc.
+'DifferenceEngineNewHeader': Allows extensions to change the $newHeader
+variable, which contains information about the new revision, such as the
+revision's author, whether the revision was marked as a minor edit or not, etc.
 $differenceEngine: DifferenceEngine object
 &$newHeader: The string containing the various #mw-diff-otitle[1-5] divs, which
-include things like revision author info, revision comment, RevisionDelete link and more
+  include things like revision author info, revision comment, RevisionDelete
+  link and more
 $formattedRevisionTools: Array containing revision tools, some of which may have
-been injected with the DiffRevisionTools hook
-$nextlink: String containing the link to the next revision (if any); also included in $newHeader
-$rollback: Rollback link (string) to roll this revision back to the previous one, if any
+  been injected with the DiffRevisionTools hook
+$nextlink: String containing the link to the next revision (if any); also
+  included in $newHeader
+$rollback: Rollback link (string) to roll this revision back to the previous
+  one, if any
 $newminor: String indicating if the new revision was marked as a minor edit
 $diffOnly: Boolean parameter passed to DifferenceEngine#showDiffPage, indicating
-whether we should show just the diff; passed in as a query string parameter to the
-various URLs constructed here (i.e. $nextlink)
+  whether we should show just the diff; passed in as a query string parameter to
+  the various URLs constructed here (i.e. $nextlink)
 $rdel: RevisionDelete link for the new revision, if the current user is allowed
-to use the RevisionDelete feature
+  to use the RevisionDelete feature
 $unhide: Boolean parameter indicating whether to show RevisionDeleted revisions
 
-'DifferenceEngineOldHeader': Allows extensions to change the $oldHeader variable, which
-contains information about the old revision, such as the revision's author, whether
-the revision was marked as a minor edit or not, etc.
+'DifferenceEngineOldHeader': Allows extensions to change the $oldHeader
+variable, which contains information about the old revision, such as the
+revision's author, whether the revision was marked as a minor edit or not, etc.
 $differenceEngine: DifferenceEngine object
 &$oldHeader: The string containing the various #mw-diff-otitle[1-5] divs, which
-include things like revision author info, revision comment, RevisionDelete link and more
-$prevlink: String containing the link to the previous revision (if any); also included in $oldHeader
+  include things like revision author info, revision comment, RevisionDelete
+  link and more
+$prevlink: String containing the link to the previous revision (if any); also
+  included in $oldHeader
 $oldminor: String indicating if the old revision was marked as a minor edit
 $diffOnly: Boolean parameter passed to DifferenceEngine#showDiffPage, indicating
-whether we should show just the diff; passed in as a query string parameter to the
-various URLs constructed here (i.e. $prevlink)
+  whether we should show just the diff; passed in as a query string parameter to
+  the various URLs constructed here (i.e. $prevlink)
 $ldel: RevisionDelete link for the old revision, if the current user is allowed
-to use the RevisionDelete feature
+  to use the RevisionDelete feature
 $unhide: Boolean parameter indicating whether to show RevisionDeleted revisions
 
-'DifferenceEngineOldHeaderNoOldRev': Change the $oldHeader variable in cases when
-there is no old revision
+'DifferenceEngineOldHeaderNoOldRev': Change the $oldHeader variable in cases
+when there is no old revision
 &$oldHeader: empty string by default
 
-'DifferenceEngineRenderRevisionAddParserOutput': Allows extensions to change the parser output.
-Return false to not add parser output via OutputPage's addParserOutput method.
+'DifferenceEngineRenderRevisionAddParserOutput': Allows extensions to change the
+parser output. Return false to not add parser output via OutputPage's
+addParserOutput method.
 $differenceEngine: DifferenceEngine object
 $out: OutputPage object
 $parserOutput: ParserOutput object
 $wikiPage: WikiPage object
 
-'DifferenceEngineRenderRevisionShowFinalPatrolLink': An extension can hook into this hook
-point and return false to not show the final "mark as patrolled" link on the bottom
-of a page.
+'DifferenceEngineRenderRevisionShowFinalPatrolLink': An extension can hook into
+this hook point and return false to not show the final "mark as patrolled" link
+on the bottom of a page.
 This hook has no arguments.
 
 'DifferenceEngineShowDiff': Allows extensions to affect the diff text which
 eventually gets sent to the OutputPage object.
 $differenceEngine: DifferenceEngine object
 
-'DifferenceEngineShowEmptyOldContent': Allows extensions to change the diff table
-body (without header) in cases when there is no old revision or the old and new
-revisions are identical.
+'DifferenceEngineShowEmptyOldContent': Allows extensions to change the diff
+table body (without header) in cases when there is no old revision or the old
+and new revisions are identical.
 $differenceEngine: DifferenceEngine object
 
-'DifferenceEngineShowDiffPage': Add additional output via the available OutputPage
-object into the diff view
+'DifferenceEngineShowDiffPage': Add additional output via the available
+OutputPage object into the diff view
 $out: OutputPage object
 
 'DifferenceEngineShowDiffPageMaybeShowMissingRevision': called in
@@ -1361,7 +1375,7 @@ an article
 &$article: article (object) being viewed
 &$oldid: oldid (int) being viewed
 
-'DoEditSectionLink': DEPRECATED! Use SkinEditSectionLinks instead.
+'DoEditSectionLink': DEPRECATED since 1.25! Use SkinEditSectionLinks instead.
 Override the HTML generated for section edit links
 $skin: Skin object rendering the UI
 $title: Title object for the title being linked to (may not be the same as
@@ -1381,7 +1395,6 @@ $section: Section being edited
 &$error: Error message to return
 $summary: Edit summary for page
 
-
 'EditFilterMergedContent': Post-section-merge edit filter.
 This may be triggered by the EditPage or any other facility that modifies page
 content. Use the $status object to indicate whether the edit should be allowed,
@@ -1389,7 +1402,7 @@ and to provide a reason for disallowing it. Return false to abort the edit, and
 true to continue. Returning true if $status->isOK() returns false means "don't
 save but continue user interaction", e.g. show the edit form.
 $status->apiHookResult can be set to an array to be returned by api.php
-action=edit. This is used to deliver captchas.
+  action=edit. This is used to deliver captchas.
 $context: object implementing the IContextSource interface.
 $content: content of the edit box, as a Content object.
 $status: Status object to represent errors, etc.
@@ -1417,9 +1430,9 @@ $resultDetails: Result details array
 
 'EditPage::importFormData': allow extensions to read additional data
 posted in the form
+Return value is ignored (should always return true)
 $editpage: EditPage instance
 $request: Webrequest
-return value is ignored (should always return true)
 
 'EditPage::showEditForm:fields': allows injection of form field into edit form
 Return value is ignored (should always return true)
@@ -1463,8 +1476,9 @@ textarea in the edit form.
 
 'EditPageBeforeEditToolbar': Allows modifying the edit toolbar above the
 textarea in the edit form.
+Hook subscribers can return false to avoid the default toolbar code being
+loaded.
 &$toolbar: The toolbar HTML
-Hook subscribers can return false to avoid the default toolbar code being loaded.
 
 'EditPageCopyrightWarning': Allow for site and per-namespace customization of
 contribution/copyright notice.
@@ -1475,8 +1489,8 @@ $title: title of page being edited
 'EditPageGetCheckboxesDefinition': Allows modifying the edit checkboxes
 below the textarea in the edit form.
 $editpage: The current EditPage object
-&$checkboxes: Array of checkbox definitions. See EditPage::getCheckboxesDefinition()
-for the format.
+&$checkboxes: Array of checkbox definitions. See
+  EditPage::getCheckboxesDefinition() for the format.
 
 'EditPageGetDiffContent': Allow modifying the wikitext that will be used in
 "Show changes". Note that it is preferable to implement diff handling for
@@ -1512,7 +1526,8 @@ true to allow those checks to occur, and false if checking is done.
 &$from: MailAddress object of sending user
 &$subject: subject of the mail
 &$text: text of the mail
-&$error: Out-param for an error. Should be set to a Status object or boolean false.
+&$error: Out-param for an error. Should be set to a Status object or boolean
+  false.
 
 'EmailUserCC': Before sending the copy of the email to the author.
 &$to: MailAddress object of receiving user
@@ -1547,7 +1562,8 @@ $block: The RecentChanges objects in that block
 a grouped recent change inner line in EnhancedChangesList.
 Hook subscribers can return false to omit this line from recentchanges.
 $changesList: EnhancedChangesList object
-&$data: An array with all the components that will be joined in order to create the line
+&$data: An array with all the components that will be joined in order to create
+  the line
 $block: An array of RecentChange objects in that block
 $rc: The RecentChange object for this line
 &$classes: An array of classes to change
@@ -1558,7 +1574,8 @@ $rc: The RecentChange object for this line
 'EnhancedChangesListModifyBlockLineData': to alter data used to build
 a non-grouped recent change line in EnhancedChangesList.
 $changesList: EnhancedChangesList object
-&$data: An array with all the components that will be joined in order to create the line
+&$data: An array with all the components that will be joined in order to create
+  the line
 $rc: The RecentChange object for this line
 
 'ExemptFromAccountCreationThrottle': Exemption from the account creation
@@ -1664,10 +1681,10 @@ underscore) magic words. Called by MagicWord.
 
 'GetExtendedMetadata': Get extended file metadata for the API
 &$combinedMeta: Array of the form:
-       'MetadataPropName' => array(
+       'MetadataPropName' => [
                value' => prop value,
                'source' => 'name of hook'
-       ).
+        ].
 $file: File object of file in question
 $context: RequestContext (including language to use)
 $single: Only extract the current language; if false, the prop value should
@@ -1765,7 +1782,7 @@ $lang: Language that will be used to render the timestamp
 'getUserPermissionsErrors': Add a permissions error when permissions errors are
 checked for. Use instead of userCan for most cases. Return false if the user
 can't do it, and populate $result with the reason in the form of
-array( messagename, param1, param2, ... ) or a MessageSpecifier instance (you
+[ messagename, param1, param2, ... ] or a MessageSpecifier instance (you
 might want to use ApiMessage to provide machine-readable details for the API).
 For consistency, error messages
 should be plain text with no special coloring, bolding, etc. to show that
@@ -1779,12 +1796,12 @@ $action: Action being checked
 'getUserPermissionsErrorsExpensive': Equal to getUserPermissionsErrors, but is
 called only if expensive checks are enabled. Add a permissions error when
 permissions errors are checked for. Return false if the user can't do it, and
-populate $result with the reason in the form of array( messagename, param1,
-param2, ... ) or a MessageSpecifier instance (you might want to use ApiMessage
-to provide machine-readable details for the API). For consistency, error
-messages should be plain text with no
-special coloring, bolding, etc. to show that they're errors; presenting them
-properly to the user as errors is done by the caller.
+populate $result with the reason in the form of [ messagename, param1, param2,
+... ] or a MessageSpecifier instance (you might want to use ApiMessage to
+provide machine-readable details for the API). For consistency, error messages
+should be plain text with no special coloring, bolding, etc. to show that
+they're errors; presenting them properly to the user as errors is done by the
+caller.
 &$title: Title object being checked against
 &$user: Current user object
 $action: Action being checked
@@ -1818,6 +1835,9 @@ just modify a few things using call-by-reference.
   includes/Linker.php for Linker::makeImageLink
 &$time: Timestamp of file in 'YYYYMMDDHHIISS' string form, or false for current
 &$res: Final HTML output, used if you return false
+$parser: Parser instance
+&$query: Query params for desc URL
+&$widthOption: Used by the parser to remember the user preference thumbnailsize
 
 'ImageOpenShowImageInlineBefore': Call potential extension just before showing
 the image on an image page.
@@ -1878,9 +1898,9 @@ $revisionInfo: Array of revision information
 Return false to stop further processing of the tag
 $reader: XMLReader object
 
-'ImportHandleUnknownUser': When a user doesn't exist locally, this hook is called
-to give extensions an opportunity to auto-create it. If the auto-creation is
-successful, return false.
+'ImportHandleUnknownUser': When a user doesn't exist locally, this hook is
+called to give extensions an opportunity to auto-create it. If the auto-creation
+is successful, return false.
 $name: User name
 
 'ImportHandleUploadXMLTag': When parsing a XML tag in a file upload.
@@ -1964,7 +1984,7 @@ $user: User the password is being validated for
 $code: The language code or the language we're looking for a messages file for
 &$file: The messages file path, you can override this to change the location.
 
-'LanguageGetMagic': DEPRECATED! Use $magicWords in a file listed in
+'LanguageGetMagic': DEPRECATED since 1.16! Use $magicWords in a file listed in
 $wgExtensionMessagesFiles instead.
 Use this to define synonyms of magic words depending of the language
 &$magicExtensions: associative array of magic words synonyms
@@ -1999,11 +2019,11 @@ $title: The page's Title.
 $out: The output page.
 $cssClassName: CSS class name of the language selector.
 
-'LinkBegin': DEPRECATED! Use HtmlPageLinkRendererBegin instead.
-Used when generating internal and interwiki links in
-Linker::link(), before processing starts.  Return false to skip default
-processing and return $ret. See documentation for Linker::link() for details on
-the expected meanings of parameters.
+'LinkBegin': DEPRECATED since 1.28! Use HtmlPageLinkRendererBegin instead.
+Used when generating internal and interwiki links in Linker::link(), before
+processing starts.  Return false to skip default processing and return $ret. See
+documentation for Linker::link() for details on the expected meanings of
+parameters.
 $skin: the Skin object
 $target: the Title that the link is pointing to
 &$html: the contents that the <a> tag should have (raw HTML); null means
@@ -2017,7 +2037,7 @@ $target: the Title that the link is pointing to
 &$options: array of options.  Can include 'known', 'broken', 'noclasses'.
 &$ret: the value to return if your hook returns false.
 
-'LinkEnd': DEPRECATED! Use HtmlPageLinkRendererEnd hook instead
+'LinkEnd': DEPRECATED since 1.28! Use HtmlPageLinkRendererEnd hook instead
 Used when generating internal and interwiki links in Linker::link(),
 just before the function returns a value.  If you return true, an <a> element
 with HTML attributes $attribs and contents $html will be returned.  If you
@@ -2199,11 +2219,11 @@ in LoginForm::$validErrorMessages).
 &$messages: Already added messages (inclusive messages from
   LoginForm::$validErrorMessages)
 
-'LoginUserMigrated': DEPRECATED! Create a PreAuthenticationProvider instead.
-Called during login to allow extensions the opportunity to inform a user that
-their username doesn't exist for a specific reason, instead of letting the
-login form give the generic error message that the account does not exist. For
-example, when the account has been renamed or deleted.
+'LoginUserMigrated': DEPRECATED since 1.27! Create a PreAuthenticationProvider
+instead. Called during login to allow extensions the opportunity to inform a
+user that their username doesn't exist for a specific reason, instead of letting
+the login form give the generic error message that the account does not exist.
+For example, when the account has been renamed or deleted.
 $user: the User object being authenticated against.
 &$msg: the message identifier for abort reason, or an array to pass a message
   key and parameters.
@@ -2363,8 +2383,8 @@ $new: the ?new= param value from the url
 'NewPagesLineEnding': Called before a NewPages line is finished.
 $page: the SpecialNewPages object
 &$ret: the HTML line
-$row: the database row for this page (the recentchanges record and a few extras - see
-  NewPagesPager::getQueryInfo)
+$row: the database row for this page (the recentchanges record and a few extras
+  - see NewPagesPager::getQueryInfo)
 &$classes: the classes to add to the surrounding <li>
 &$attribs: associative array of other HTML attributes for the <li> element.
   Currently only data attributes reserved to MediaWiki are allowed
@@ -2603,7 +2623,7 @@ cache or return false to not use it.
 &$parser: Parser object
 &$varCache: variable cache (array)
 
-'ParserLimitReport': DEPRECATED! Use ParserLimitReportPrepare and
+'ParserLimitReport': DEPRECATED since 1.22! Use ParserLimitReportPrepare and
 ParserLimitReportFormat instead.
 Called at the end of Parser:parse() when the parser will
 include comments about size of the text parsed.
@@ -2640,7 +2660,8 @@ change the default value for an option, all existing parser cache entries will
 be invalid. To avoid bugs, you'll need to handle that somehow (e.g. with the
 RejectParserCacheValue hook) because MediaWiki won't do it for you.
 &$defaults: Set the default value for your option here.
-&$inCacheKey: To fragment the parser cache on your option, set a truthy value here.
+&$inCacheKey: To fragment the parser cache on your option, set a truthy value
+  here.
 &$lazyLoad: To lazy-initialize your option, set it null in $defaults and set a
   callable here. The callable is passed the ParserOptions object and the option
   name.
@@ -2670,7 +2691,8 @@ run. Use when page save hooks require the presence of custom tables to ensure
 that tests continue to run properly.
 &$tables: array of table names
 
-'ParserOutputStashForEdit': Called when an edit stash parse finishes, before the output is cached.
+'ParserOutputStashForEdit': Called when an edit stash parse finishes, before the
+output is cached.
 $page: the WikiPage of the candidate edit
 $content: the Content object of the candidate edit
 $output: the ParserOutput result of the candidate edit
@@ -2732,7 +2754,8 @@ $key: the section name
 &$legend: the legend text. Defaults to wfMessage( "prefs-$key" )->text() but may
   be overridden
 
-'PrefixSearchBackend': DEPRECATED! Override SearchEngine::completionSearchBackend instead.
+'PrefixSearchBackend': DEPRECATED since 1.27! Override
+SearchEngine::completionSearchBackend instead.
 Override the title prefix search used for OpenSearch and
 AJAX search suggestions. Put results into &$results outparam and return false.
 $ns: array of int namespace keys to search in
@@ -2828,7 +2851,7 @@ $context: ResourceLoaderContext|null
 ResourceLoaderStartUpModule::getConfigSettings(). Use this to export static
 configuration variables to JavaScript. Things that depend on the current page
 or request state must be added through MakeGlobalVariablesScript instead.
-&$vars: array( variable name => value )
+&$vars: [ variable name => value ]
 
 'ResourceLoaderJqueryMsgModuleMagicWords': Called in
 ResourceLoaderJqueryMsgModule to allow adding magic words for jQueryMsg.
@@ -2847,10 +2870,10 @@ called after the addition of 'qunit' and MediaWiki testing resources.
 &$testModules: array of JavaScript testing modules. The 'qunit' framework,
   included in core, is fed using tests/qunit/QUnitTestResources.php.
   To add a new qunit module named 'myext.tests':
-       $testModules['qunit']['myext.tests'] = array(
+       $testModules['qunit']['myext.tests'] = [
                'script' => 'extension/myext/tests.js',
                'dependencies' => <any module dependency you might have>
-       );
+        ];
   For QUnit framework, the mediawiki.tests.qunit.testrunner dependency will be
   added to any module.
 &$ResourceLoader: object
@@ -2858,8 +2881,8 @@ called after the addition of 'qunit' and MediaWiki testing resources.
 'RevisionRecordInserted': Called after a revision is inserted into the database.
 $revisionRecord: the RevisionRecord that has just been inserted.
 
-'RevisionInsertComplete': DEPRECATED! Use RevisionRecordInserted hook instead.
-Called after a revision is inserted into the database.
+'RevisionInsertComplete': DEPRECATED since 1.31! Use RevisionRecordInserted hook
+instead. Called after a revision is inserted into the database.
 $revision: the Revision
 $data: DEPRECATED! Always null!
 $flags: DEPRECATED! Always null!
@@ -2907,10 +2930,12 @@ $engine: SearchEngine for which the indexing is intended
 
 'SearchResultsAugment': Allows extension to add its code to the list of search
 result augmentors.
-&$setAugmentors: List of whole-set augmentor objects, must implement ResultSetAugmentor
-&$rowAugmentors: List of per-row augmentor objects, must implement ResultAugmentor.
-Note that lists should be in the format name => object and the names in both lists should
-be distinct.
+&$setAugmentors: List of whole-set augmentor objects, must implement
+  ResultSetAugmentor.
+&$rowAugmentors: List of per-row augmentor objects, must implement
+  ResultAugmentor.
+Note that lists should be in the format name => object and the names in both
+  lists should be distinct.
 
 'SecondaryDataUpdates': Allows modification of the list of DataUpdates to
 perform when page content is modified. Currently called by
@@ -2993,11 +3018,13 @@ $terms: Search terms, for highlighting
 
 'ShowSearchHitTitle': Customise display of search hit title/link.
 &$title: Title to link to
-&$titleSnippet: Label for the link representing the search result. Typically the article title.
+&$titleSnippet: Label for the link representing the search result. Typically the
+  article title.
 $result: The SearchResult object
 $terms: String of the search terms entered
 $specialSearch: The SpecialSearch object
-&$query: Array of query string parameters for the link representing the search result.
+&$query: Array of query string parameters for the link representing the search
+  result.
 &$attributes: Array of title link attributes, can be modified by extension.
 
 'SidebarBeforeOutput': Allows to edit sidebar just before it is output by skins.
@@ -3175,7 +3202,7 @@ $pager: The UsersPager instance
 
 'SpecialListusersFormatRow': Called right before the end of
 UsersPager::formatRow().
-&$item: HTML to be returned. Will be wrapped in <li></li> after the hook finishes
+&$item: HTML to be returned. Will be wrapped in an <li> after the hook finishes
 $row: Database row object
 
 'SpecialListusersHeader': Called after adding the submit button in
@@ -3252,8 +3279,8 @@ use this to change some selection criteria or substitute a different title.
 &$title: If the hook returns false, a Title object to use instead of the
   result from the normal query
 
-'SpecialRecentChangesFilters': DEPRECATED! Use ChangesListSpecialPageStructuredFilters
-instead.
+'SpecialRecentChangesFilters': DEPRECATED since 1.23! Use
+ChangesListSpecialPageStructuredFilters instead.
 Called after building form options at RecentChanges.
 $special: the special page object
 &$filters: associative array of filter definitions. The keys are the HTML
@@ -3265,8 +3292,8 @@ SpecialRecentChanges.
 &$extraOpts: array of added items, to which can be added
 $opts: FormOptions for this request
 
-'SpecialRecentChangesQuery': DEPRECATED! Use ChangesListSpecialPageStructuredFilters
-or ChangesListSpecialPageQuery instead.
+'SpecialRecentChangesQuery': DEPRECATED since 1.23! Use
+ChangesListSpecialPageStructuredFilters or ChangesListSpecialPageQuery instead.
 Called when building SQL query for SpecialRecentChanges and
 SpecialRecentChangesLinked.
 &$conds: array of WHERE conditionals for query
@@ -3279,10 +3306,10 @@ $opts: FormOptions for this request
 'SpecialResetTokensTokens': Called when building token list for
 SpecialResetTokens.
 &$tokens: array of token information arrays in the format of
-       array(
+       [
                'preference' => '<preference-name>',
                'label-message' => '<message-key>',
-       )
+        ]
 
 'SpecialSearchCreateLink': Called when making the message to create a page or
 go to the existing page.
@@ -3295,7 +3322,8 @@ $url does a standard redirect to $title. Setting $url redirects to the
 specified URL.
 $term: The string the user searched for
 $title: The title the 'go' feature has decided to forward the user to
-&$url: Initially null, hook subscribers can set this to specify the final url to redirect to
+&$url: Initially null, hook subscribers can set this to specify the final url to
+  redirect to
 
 'SpecialSearchNogomatch': Called when the 'Go' feature is triggered (generally
 from autocomplete search other than the main bar on Special:Search) and the
@@ -3353,11 +3381,14 @@ $engine: the search engine
   message key to use in the name column,
 $context: IContextSource object
 
-'SpecialTrackingCategories::preprocess': Called after LinkBatch on Special:TrackingCategories
+'SpecialTrackingCategories::preprocess': Called after LinkBatch on
+Special:TrackingCategories
 $specialPage: The SpecialTrackingCategories object
-$trackingCategories: Array of data from Special:TrackingCategories with msg and cats
+$trackingCategories: Array of data from Special:TrackingCategories with msg and
+  cats
 
-'SpecialTrackingCategories::generateCatLink': Called for each cat link on Special:TrackingCategories
+'SpecialTrackingCategories::generateCatLink': Called for each cat link on
+Special:TrackingCategories
 $specialPage: The SpecialTrackingCategories object
 $catTitle: The Title object of the linked category
 &$html: The Result html
@@ -3370,8 +3401,8 @@ Special:Upload.
 $wgVersion: Current $wgVersion for you to use
 &$versionUrl: Raw url to link to (eg: release notes)
 
-'SpecialWatchlistFilters': DEPRECATED! Use ChangesListSpecialPageStructuredFilters
-instead.
+'SpecialWatchlistFilters': DEPRECATED since 1.23! Use
+ChangesListSpecialPageStructuredFilters instead.
 Called after building form options at Watchlist.
 $special: the special page object
 &$filters: associative array of filter definitions. The keys are the HTML
@@ -3383,8 +3414,8 @@ SpecialWatchlist. Allows extensions to register custom values they have
 inserted to rc_type so they can be returned as part of the watchlist.
 &$nonRevisionTypes: array of values in the rc_type field of recentchanges table
 
-'SpecialWatchlistQuery': DEPRECATED! Use ChangesListSpecialPageStructuredFilters
-or ChangesListSpecialPageQuery instead.
+'SpecialWatchlistQuery': DEPRECATED since 1.23! Use
+ChangesListSpecialPageStructuredFilters or ChangesListSpecialPageQuery instead.
 Called when building sql query for SpecialWatchlist.
 &$conds: array of WHERE conditionals for query
 &$tables: array of tables to be queried
@@ -3447,7 +3478,8 @@ $old: old title
 $nt: new title
 $user: user who does the move
 
-'TitleMoveStarting': Before moving an article (title), but just after the atomic DB section starts.
+'TitleMoveStarting': Before moving an article (title), but just after the atomic
+DB section starts.
 $old: old title
 $nt: new title
 $user: user who does the move
@@ -3520,8 +3552,8 @@ $title: Title object of the page that we're about to undelete
 $title: title object related to the revision
 $rev: revision (object) that will be viewed
 
-'UnitTestsAfterDatabaseSetup': Called right after MediaWiki's test infrastructure
-has finished creating/duplicating core tables for unit tests.
+'UnitTestsAfterDatabaseSetup': Called right after MediaWiki's test
+infrastructure has finished creating/duplicating core tables for unit tests.
 $database: Database in question
 $prefix: Table prefix to be used in unit tests
 
@@ -3533,7 +3565,7 @@ Since 1.24: Paths pointing to a directory will be recursively scanned for
 test case files matching the suffix "Test.php".
 &$paths: list of test cases and directories to search.
 
-'UnknownAction': DEPRECATED! To add an action in an extension,
+'UnknownAction': DEPRECATED since 1.19! To add an action in an extension,
 create a subclass of Action, and add a new key to $wgActions.
 An unknown "action" has occurred (useful for defining your own actions).
 $action: action name
@@ -3560,11 +3592,11 @@ $type: (string) the requested upload type
 &$className: the class name of the Upload instance to be created
 
 'UploadForm:BeforeProcessing': At the beginning of processUpload(). Lets you
-poke at member variables like $mUploadDescription before the file is saved. Do
-not use this hook to break upload processing. This will return the user to a
-blank form with no error message; use UploadVerification and UploadVerifyFile
-instead.
-&$form: UploadForm object
+poke at member variables like $mUploadDescription before the file is saved.
+Do not use this hook to break upload processing.
+This will return the user to a blank form with no error message;
+use UploadVerifyUpload or UploadVerifyFile instead.
+&$upload: SpecialUpload object
 
 'UploadForm:getInitialPageText': After the initial page text for file uploads
 is generated, to allow it to be altered.
@@ -3575,7 +3607,7 @@ $config: Config object
 'UploadForm:initial': Before the upload form is generated. You might set the
 member-variables $uploadFormTextTop and $uploadFormTextAfterSummary to inject
 text (HTML) either before or after the editform.
-&$form: UploadForm object
+&$upload: SpecialUpload object
 
 'UploadFormInitDescriptor': After the descriptor for the upload form as been
 assembled.
@@ -3594,12 +3626,12 @@ being uploaded, use UploadVerifyFile or UploadVerifyUpload.
 $upload: (object) An instance of UploadBase, with all info about the upload
 $user: (object) An instance of User, the user uploading this file
 $props: (array) File properties, as returned by FSFile::getPropsFromPath()
-&$error: output: If the file stashing should be prevented, set this to the reason
-  in the form of array( messagename, param1, param2, ... ) or a MessageSpecifier
-  instance (you might want to use ApiMessage to provide machine-readable details
-  for the API).
+&$error: output: If the file stashing should be prevented, set this to the
+  reason in the form of [ messagename, param1, param2, ... ] or a
+  MessageSpecifier instance (you might want to use ApiMessage to provide machine
+  -readable details for the API).
 
-'UploadVerification': DEPRECATED! Use UploadVerifyFile instead.
+'UploadVerification': DEPRECATED since 1.28! Use UploadVerifyFile instead.
 Additional chances to reject an uploaded file.
 $saveName: (string) destination file name
 $tempName: (string) filesystem path to the temporary file for checks
@@ -3612,10 +3644,10 @@ in most cases over UploadVerification.
 $upload: (object) an instance of UploadBase, with all info about the upload
 $mime: (string) The uploaded file's MIME type, as detected by MediaWiki.
   Handlers will typically only apply for specific MIME types.
-&$error: (object) output: true if the file is valid. Otherwise, set this to the reason
-  in the form of array( messagename, param1, param2, ... ) or a MessageSpecifier
-  instance (you might want to use ApiMessage to provide machine-readable details
-  for the API).
+&$error: (object) output: true if the file is valid. Otherwise, set this to the
+  reason in the form of [ messagename, param1, param2, ... ] or a
+  MessageSpecifier instance (you might want to use ApiMessage to provide machine
+  -readable details for the API).
 
 'UploadVerifyUpload': Upload verification, based on both file properties like
 MIME type (same as UploadVerifyFile) and the information entered by the user
@@ -3626,7 +3658,7 @@ $props: (array) File properties, as returned by FSFile::getPropsFromPath()
 $comment: (string) Upload log comment (also used as edit summary)
 $pageText: (string) File description page text (only used for new uploads)
 &$error: output: If the file upload should be prevented, set this to the reason
-  in the form of array( messagename, param1, param2, ... ) or a MessageSpecifier
+  in the form of [ messagename, param1, param2, ... ] or a MessageSpecifier
   instance (you might want to use ApiMessage to provide machine-readable details
   for the API).
 
@@ -3669,8 +3701,8 @@ messages!" message, return false to not delete it.
 &$user: User (object) that will clear the message
 $oldid: ID of the talk page revision being viewed (0 means the most recent one)
 
-'UserCreateForm': DEPRECATED! Create an AuthenticationProvider instead.
-Manipulate the login form.
+'UserCreateForm': DEPRECATED since 1.27! Create an AuthenticationProvider
+instead. Manipulate the login form.
 &$template: SimpleTemplate instance for the form
 
 'UserEffectiveGroups': Called in User::getEffectiveGroups().
@@ -3697,7 +3729,7 @@ $user: User object
 &$timestamp: timestamp, change this to override local email authentication
   timestamp
 
-'UserGetImplicitGroups': DEPRECATED!
+'UserGetImplicitGroups': DEPRECATED since 1.25!
 Called in User::getImplicitGroups().
 &$groups: List of implicit (automatically-assigned) groups
 
@@ -3763,7 +3795,8 @@ $name: user name
 $user: user object
 &$s: database query object
 
-'UserLoadFromSession': DEPRECATED! Create a MediaWiki\Session\SessionProvider instead.
+'UserLoadFromSession': DEPRECATED since 1.27! Create a
+MediaWiki\Session\SessionProvider instead.
 Called to authenticate users on external/environmental means; occurs before
 session is loaded.
 $user: user object being loaded
@@ -3778,16 +3811,17 @@ $user: User object
 'UserLoggedIn': Called after a user is logged in
 $user: User object for the logged-in user
 
-'UserLoginComplete': Show custom content after a user has logged in via the web interface.
-For functionality that needs to run after any login (API or web) use UserLoggedIn.
+'UserLoginComplete': Show custom content after a user has logged in via the Web
+interface. For functionality that needs to run after any login (API or web) use
+UserLoggedIn.
 &$user: the user object that was created on login
 &$inject_html: Any HTML to inject after the "logged in" message.
-$direct: (bool) The hook is called directly after a successful login. This will only happen once
-  per login. A UserLoginComplete call with direct=false can happen when the user visits the login
-  page while already logged in.
+$direct: (bool) The hook is called directly after a successful login. This will
+  only happen once per login. A UserLoginComplete call with direct=false can
+  happen when the user visits the login page while already logged in.
 
-'UserLoginForm': DEPRECATED! Create an AuthenticationProvider instead.
-Manipulate the login form.
+'UserLoginForm': DEPRECATED since 1.27! Create an AuthenticationProvider
+instead. Manipulate the login form.
 &$template: QuickTemplate instance for the form
 
 'UserLogout': Before a user logs out.
@@ -3805,21 +3839,26 @@ $to: Array of MailAddress objects for the recipients
 
 'UserMailerSplitTo': Called in UserMailer::send() to give extensions a chance
 to split up an email with multiple the To: field into separate emails.
-&$to: array of MailAddress objects; unset the ones which should be mailed separately
+&$to: array of MailAddress objects; unset the ones which should be mailed
+separately
 
-'UserMailerTransformContent': Called in UserMailer::send() to change email contents.
-Extensions can block sending the email by returning false and setting $error.
+'UserMailerTransformContent': Called in UserMailer::send() to change email
+contents. Extensions can block sending the email by returning false and setting
+$error.
 $to: array of MailAdresses of the targets
 $from: MailAddress of the sender
-&$body: email body, either a string (for plaintext emails) or an array with 'text' and 'html' keys
+&$body: email body, either a string (for plaintext emails) or an array with
+  'text' and 'html' keys
 &$error: should be set to an error message string
 
-'UserMailerTransformMessage': Called in UserMailer::send() to change email after it has gone through
-the MIME transform. Extensions can block sending the email by returning false and setting $error.
+'UserMailerTransformMessage': Called in UserMailer::send() to change email after
+it has gone through the MIME transform. Extensions can block sending the email
+by returning false and setting $error.
 $to: array of MailAdresses of the targets
 $from: MailAddress of the sender
 &$subject: email subject (not MIME encoded)
-&$headers: email headers (except To: and Subject:) as an array of header name => value pairs
+&$headers: email headers (except To: and Subject:) as an array of header
+name => value pairs
 &$body: email body (in MIME format) as a string
 &$error: should be set to an error message string
 
@@ -3848,27 +3887,29 @@ message(s).
 &$user: user retrieving new talks messages
 &$talks: array of new talks page(s)
 
-'UserRights': DEPRECATED! Use UserGroupsChanged instead.
+'UserRights': DEPRECATED since 1.26! Use UserGroupsChanged instead.
 After a user's group memberships are changed.
 &$user: User object that was changed
 $add: Array of strings corresponding to groups added
 $remove: Array of strings corresponding to groups removed
 
-'UserSaveOptions': Called just before saving user preferences. Hook handlers can either add or
-manipulate options, or reset one back to it's default to block changing it. Hook handlers are also
-allowed to abort the process by returning false, e.g. to save to a global profile instead. Compare
-to the UserSaveSettings hook, which is called after the preferences have been saved.
+'UserSaveOptions': Called just before saving user preferences. Hook handlers can
+either add or manipulate options, or reset one back to it's default to block
+changing it. Hook handlers are also allowed to abort the process by returning
+false, e.g. to save to a global profile instead. Compare to the UserSaveSettings
+hook, which is called after the preferences have been saved.
 $user: The User for which the options are going to be saved
 &$options: The users options as an associative array, modifiable
 
-'UserSaveSettings': Called directly after user preferences (user_properties in the database) have
-been saved. Compare to the UserSaveOptions hook, which is called before.
+'UserSaveSettings': Called directly after user preferences (user_properties in
+the database) have been saved. Compare to the UserSaveOptions hook, which is
+called before.
 $user: The User for which the options have been saved
 
-'UserSetCookies': DEPRECATED! If you're trying to replace core session cookie
-handling, you want to create a subclass of MediaWiki\Session\CookieSessionProvider
-instead. Otherwise, you can no longer count on user data being saved to cookies
-versus some other mechanism.
+'UserSetCookies': DEPRECATED since 1.27! If you're trying to replace core
+session cookie handling, you want to create a subclass of
+MediaWiki\Session\CookieSessionProvider instead. Otherwise, you can no longer
+count on user data being saved to cookies versus some other mechanism.
 Called when setting user cookies.
 $user: User object
 &$session: session array, will be added to the session
@@ -3897,7 +3938,7 @@ displayed correctly in Special:ListUsers.
 $dbr: Read-only database handle
 $userIds: Array of user IDs whose groups we should look up
 &$cache: Array of user ID -> (array of internal group name (e.g. 'sysop') ->
-UserGroupMembership object)
+  UserGroupMembership object)
 &$groups: Array of group name -> bool true mappings for members of a given user
 group
 
@@ -3972,15 +4013,15 @@ dumps. One, and only one hook should set this, and return false.
 &$opts: Options to use for the query
 &$join: Join conditions
 
-'WikiPageDeletionUpdates': manipulate the list of DeferrableUpdates to be applied when
-a page is deleted. Called in WikiPage::getDeletionUpdates(). Note that updates
-specific to a content model should be provided by the respective Content's
-getDeletionUpdates() method.
+'WikiPageDeletionUpdates': manipulate the list of DeferrableUpdates to be
+applied when a page is deleted. Called in WikiPage::getDeletionUpdates(). Note
+that updates specific to a content model should be provided by the respective
+Content's getDeletionUpdates() method.
 $page: the WikiPage
-$content: the Content to generate updates for, or null in case the page revision could not be
-  loaded. The delete will succeed despite this.
-&$updates: the array of objects that implement DeferrableUpdate. Hook function may want to add to
-  it.
+$content: the Content to generate updates for, or null in case the page revision
+  could not be loaded. The delete will succeed despite this.
+&$updates: the array of objects that implement DeferrableUpdate. Hook function
+  may want to add to it.
 
 'WikiPageFactory': Override WikiPage class used for a title
 $title: Title of the page
diff --git a/docs/pageupdater.txt b/docs/pageupdater.txt
new file mode 100644 (file)
index 0000000..4980c92
--- /dev/null
@@ -0,0 +1,191 @@
+This document provides an overview of the usage of PageUpdater and DerivedPageDataUpdater.
+
+== PageUpdater ==
+PageUpdater is the canonical way to create page revisions, that is, to perform edits.
+
+PageUpdater is a stateful, handle-like object that allows new revisions to be created
+on a given wiki page using the saveRevision() method. PageUpdater provides setters for
+defining the new revision's content as well as meta-data such as change tags. saveRevision()
+stores the new revision's primary content and metadata, and triggers the necessary
+updates to derived secondary data and cached artifacts e.g. in the ParserCache and the
+CDN layer, using a DerivedPageDataUpdater.
+
+PageUpdater instances follow the below life cycle, defined by a number of
+methods:
+
+                          +----------------------------+
+                          |                            |
+                          |             new            |
+                          |                            |
+                          +------|--------------|------+
+                                 |              |
+            grabParentRevision()-|              |
+            or hasEditConflict()-|              |
+                                 |              |
+                        +--------v-------+      |
+                        |                |      |
+                        |  parent known  |      |
+                        |                |      |
+  Enables---------------+--------|-------+      |
+    safe operations based on     |              |-saveRevision()
+    the parent revision, e.g.    |              |
+    section replacement or       |              |
+    edit conflict resolution.    |              |
+                                 |              |
+                  saveRevision()-|              |
+                                 |              |
+                          +------v--------------v------+
+                          |                            |
+                          |      creation committed    |
+                          |                            |
+  Enables-----------------+----------------------------+
+    wasSuccess()
+    isUnchanged()
+    isNew()
+    getState()
+    getNewRevision()
+    etc.
+
+The stateful nature of PageUpdater allows it to be used to safely perform
+transformations that depend on the new revision's parent revision, such as replacing
+sections or applying 3-way conflict resolution, while protecting against race
+conditions using a compare-and-swap (CAS) mechanism: after calling code used the
+grabParentRevision() method to access the edit's logical parent, PageUpdater
+remembers that revision, and ensure that that revision is still the page's current
+revision when performing the atomic database update for the revision's primary
+meta-data when saveRevision() is called. If another revision was created concurrently,
+saveRevision() will fail, indicating the problem with the "edit-conflict" code in the status
+object.
+
+Typical usage for programmatic revision creation (with $page being a WikiPage as of 1.32, to be
+replaced by a repository service later):
+
+  $updater = $page->newPageUpdater( $user );
+  $updater->setContent( 'main', $content );
+  $updater->setRcPatrolStatus( RecentChange::PRC_PATROLLED );
+  $newRev = $updater->saveRevision( $comment );
+
+Usage with content depending on the parent revision
+
+  $updater = $page->newPageUpdater( $user );
+  $parent = $updater->grabParentRevision();
+  $content = $parent->getContent( 'main' )->replaceSection( $section, $sectionContent );
+  $updater->setContent( 'main', $content );
+  $newRev = $updater->saveRevision( $comment, EDIT_UPDATE );
+
+In both cases, all secondary updates will be triggered automatically.
+
+== DerivedPageDataUpdater ==
+DerivedPageDataUpdater is a stateful, handle-like object that caches derived data representing
+a revision, and can trigger updates of cached copies of that data, e.g. in the links tables,
+page_props, the ParserCache, and the CDN layer.
+
+DerivedPageDataUpdater is used by PageUpdater when creating new revisions, but can also
+be used independently when performing meta data updates during undeletion, import, or
+when puring a page. It's a stepping stone on the way to a more complete refactoring of WikiPage.
+
+NOTE: Avoid direct usage of DerivedPageDataUpdater. In the future, we want to define interfaces
+for the different use cases of DerivedPageDataUpdater, particularly providing access to post-PST
+content and ParserOutput to callbacks during revision creation, which currently use
+WikiPage::prepareContentForEdit, and allowing updates to be triggered on purge, import, and
+undeletion, which currently use WikiPage::doEditUpdates() and Content::getSecondaryDataUpdates().
+
+The primary reason for DerivedPageDataUpdater to be stateful is internal caching of state
+that avoids the re-generation of ParserOutput and re-application of pre-save-
+transformations (PST).
+
+DerivedPageDataUpdater instances follow the below life cycle, defined by a number of
+methods:
+
+                       +---------------------------------------------------------------------+
+                       |                                                                     |
+                       |                                 new                                 |
+                       |                                                                     |
+                       +---------------|------------------|------------------|---------------+
+                                       |                  |                  |
+                 grabCurrentRevision()-|                  |                  |
+                                       |                  |                  |
+                           +-----------v----------+       |                  |
+                           |                      |       |-prepareContent() |
+                           |    knows current     |       |                  |
+                           |                      |       |                  |
+  Enables------------------+-----|-----|----------+       |                  |
+    pageExisted()                |     |                  |                  |
+    wasRedirect()                |     |-prepareContent() |                  |-prepareUpdate()
+                                 |     |                  |                  |
+                                 |     |    +-------------v------------+     |
+                                 |     |    |                          |     |
+                                 |     +---->        has content       |     |
+                                 |          |                          |     |
+  Enables------------------------|----------+--------------------------+     |
+    isChange()                   |                              |            |
+    isCreation()                 |-prepareUpdate()              |            |
+    getSlots()                   |              prepareUpdate()-|            |
+    getTouchedSlotRoles()        |                              |            |
+    getCanonicalParserOutput()   |                  +-----------v------------v-----------------+
+                                 |                  |                                          |
+                                 +------------------>                 has revision             |
+                                                    |                                          |
+  Enables-------------------------------------------+------------------------|-----------------+
+    updateParserCache()                                                      |
+    runSecondaryDataUpdates()                                                |-doUpdates()
+                                                                             |
+                                                                 +-----------v---------+
+                                                                 |                     |
+                                                                 |     updates done    |
+                                                                 |                     |
+                                                                 +---------------------+
+
+
+- grabCurrentRevision() returns the logical parent revision of the target revision. It is
+guaranteed to always return the same revision for a given DerivedPageDataUpdater instance.
+If called before prepareUpdate(), this fixates the logical parent to be the page's current
+revision. If called for the first time after prepareUpdate(), it returns the revision
+passed as the 'oldrevision' option to prepareUpdate(), or, if that wasn't given, the
+parent of $revision parameter passed to prepareUpdate().
+
+- prepareContent() is called before the new revision is created, to apply pre-save-
+transformation (PST) and allow subsequent access to the canonical ParserOutput of the
+revision. getSlots() and getCanonicalParserOutput() as well as getSecondaryDataUpdates()
+may be used after prepareContent() was called. Calling prepareContent() with the same
+parameters again has no effect. Calling it again with mismatching paramters, or calling
+it after prepareUpdate() was called, triggers a LogicException.
+
+- prepareUpdate() is called after the new revision has been created. This may happen
+right after the revision was created, on the same instance on which prepareContent() was
+called, or later (possibly much later), on a fresh instance in a different process,
+due to deferred or asynchronous updates, or during import, undeletion, purging, etc.
+prepareUpdate() is required before a call to doUpdates(), and it also enables calls to
+getSlots() and getCanonicalParserOutput() as well as getSecondaryDataUpdates().
+Calling prepareUpdate() with the same parameters again has no effect.
+Calling it again with mismatching parameters, or calling it with parameters mismatching
+the ones prepareContent() was called with, triggers a LogicException.
+
+- getSecondaryDataUpdtes() returns DataUpdates that represent derived data for the revision.
+These may be used to update such data, e.g. in ApiPurge, RefreshLinksJob, and the refreshLinks
+script.
+
+- doUpdates() triggers the updates defined by getSecondaryDataUpdtes(), and also causes
+updates to cached artifacts in the ParserCache, the CDN layer, etc. This is primarily
+used by PageUpdater, but also by PageArchive during undeletion, and when importing
+revisions from XML. doUpdates() can only be called after prepareUpdate() was used to
+initialize the DerivedPageDataUpdater instance for a specific revision. Calling it before
+prepareUpdate() is called raises a LogicException.
+
+A DerivedPageDataUpdater instance is intended to be re-used during different stages
+of complex update operations that often involve callbacks to extension code via
+MediaWiki's hook mechanism, or deferred or even asynchronous execution of Jobs and
+DeferredUpdates. Since these mechanisms typically do not provide a way to pass a
+DerivedPageDataUpdater directly, WikiPage::getDerivedPageDataUpdater() has to be used to
+obtain a DerivedPageDataUpdater for the update currently in progress - re-using the
+same DerivedPageDataUpdater if possible avoids re-generation of ParserOutput objects
+and other expensively derived artifacts.
+
+This mechanism for re-using a DerivedPageDataUpdater instance without passing it directly
+requires a way to ensure that a given DerivedPageDataUpdater instance can actually be used
+in the calling code's context. For this purpose, WikiPage::getDerivedPageDataUpdater()
+calls the isReusableFor() method on DerivedPageDataUpdater, which ensures that the given
+instance is applicable to the given parameters. In other words, isReusableFor() predicts
+whether calling prepareContent() or prepareUpdate() with a given set of parameters will
+trigger a LogicException. In that case, WikiPage::getDerivedPageDataUpdater() creates a
+fresh DerivedPageDataUpdater instance.
index b73ecbd..e12db24 100644 (file)
@@ -96,11 +96,7 @@ class AuthPlugin {
         * @return string
         */
        public function getDomain() {
-               if ( isset( $this->domain ) ) {
-                       return $this->domain;
-               } else {
-                       return 'invaliddomain';
-               }
+               return $this->domain ?? 'invaliddomain';
        }
 
        /**
index 2cf44b8..41ecc65 100644 (file)
@@ -48,7 +48,7 @@ class Category {
 
        /**
         * Set up all member variables using a database query.
-        * @param int $mode
+        * @param int $mode One of (Category::LOAD_ONLY, Category::LAZY_INIT_ROW)
         * @throws MWException
         * @return bool True on success, false on failure.
         */
index b032f2b..2c98283 100644 (file)
@@ -4736,10 +4736,10 @@ $wgPasswordDefault = 'pbkdf2';
  * $wgPasswordConfig['bcrypt-peppered'] = [
  *     'class' => EncryptedPassword::class,
  *     'underlying' => 'bcrypt',
- *     'secrets' => [],
- *     'cipher' => MCRYPT_RIJNDAEL_256,
- *     'mode' => MCRYPT_MODE_CBC,
- *     'cost' => 5,
+ *     'secrets' => [
+ *         hash( 'sha256', 'secret', true ),
+ *     ],
+ *     'cipher' => 'aes-256-cbc',
  * ];
  * @endcode
  *
@@ -8898,6 +8898,17 @@ $wgInterwikiPrefixDisplayTypes = [];
  */
 $wgCommentTableSchemaMigrationStage = MIGRATION_OLD;
 
+/**
+ * RevisionStore table schema migration stage (content, slots, content_models & slot_roles tables)
+ *
+ * @see Task: https://phabricator.wikimedia.org/T174028
+ * @see Commit: https://gerrit.wikimedia.org/r/#/c/378724/
+ *
+ * @since 1.32
+ * @var int One of the MIGRATION_* constants
+ */
+$wgMultiContentRevisionSchemaMigrationStage = MIGRATION_OLD;
+
 /**
  * Actor table schema migration stage.
  * @since 1.31
index 7958420..2f5455e 100644 (file)
@@ -5,14 +5,6 @@
  */
 class DummyLinker {
 
-       /**
-        * @deprecated since 1.28, use LinkRenderer::getLinkClasses() instead
-        */
-       public function getLinkColour( $t, $threshold ) {
-               wfDeprecated( __METHOD__, '1.28' );
-               return Linker::getLinkColour( $t, $threshold );
-       }
-
        public function link(
                $target,
                $html = null,
index 644b625..9209761 100644 (file)
@@ -486,17 +486,15 @@ class EditPage {
 
        /**
         * Get the context title object.
-        * If not set, $wgTitle will be returned. This behavior might change in
-        * the future to return $this->mTitle instead.
+        *
+        * If not set, $wgTitle will be returned, but this is deprecated. This will
+        * throw an exception.
         *
         * @return Title
         */
        public function getContextTitle() {
                if ( is_null( $this->mContextTitle ) ) {
-                       wfDebugLog(
-                               'GlobalTitleFail',
-                               __METHOD__ . ' called by ' . wfGetAllCallers( 5 ) . ' with no title set.'
-                       );
+                       wfDeprecated( __METHOD__ . ' called with no title set', '1.32' );
                        global $wgTitle;
                        return $wgTitle;
                } else {
@@ -1495,7 +1493,11 @@ class EditPage {
         * @return Status The resulting status object.
         */
        public function attemptSave( &$resultDetails = false ) {
-               # Allow bots to exempt some edits from bot flagging
+               // TODO: MCR: treat $this->minoredit like $this->bot and check isAllowed( 'minoredit' )!
+               // Also, add $this->autopatrol like $this->bot and check isAllowed( 'autopatrol' )!
+               // This is needed since PageUpdater no longer checks these rights!
+
+               // Allow bots to exempt some edits from bot flagging
                $bot = $this->context->getUser()->isAllowed( 'bot' ) && $this->bot;
                $status = $this->internalAttemptSave( $resultDetails, $bot );
 
index 9638502..89a6a10 100644 (file)
@@ -38,29 +38,6 @@ class Linker {
        const TOOL_LINKS_NOBLOCK = 1;
        const TOOL_LINKS_EMAIL = 2;
 
-       /**
-        * Return the CSS colour of a known link
-        *
-        * @deprecated since 1.28, use LinkRenderer::getLinkClasses() instead
-        *
-        * @since 1.16.3
-        * @param LinkTarget $t
-        * @param int $threshold User defined threshold
-        * @return string CSS class
-        */
-       public static function getLinkColour( LinkTarget $t, $threshold ) {
-               wfDeprecated( __METHOD__, '1.28' );
-               $services = MediaWikiServices::getInstance();
-               $linkRenderer = $services->getLinkRenderer();
-               if ( $threshold !== $linkRenderer->getStubThreshold() ) {
-                       // Need to create a new instance with the right stub threshold...
-                       $linkRenderer = $services->getLinkRendererFactory()->create();
-                       $linkRenderer->setStubThreshold( $threshold );
-               }
-
-               return $linkRenderer->getLinkClasses( $t );
-       }
-
        /**
         * This function returns an HTML link to the given target.  It serves a few
         * purposes:
@@ -328,7 +305,9 @@ class Linker {
                $res = null;
                $dummy = new DummyLinker;
                if ( !Hooks::run( 'ImageBeforeProduceHTML', [ &$dummy, &$title,
-                       &$file, &$frameParams, &$handlerParams, &$time, &$res ] ) ) {
+                       &$file, &$frameParams, &$handlerParams, &$time, &$res,
+                       $parser, &$query, &$widthOption
+               ] ) ) {
                        return $res;
                }
 
index bfbd557..73fdd82 100644 (file)
@@ -254,11 +254,7 @@ class MWNamespace {
         */
        public static function getCanonicalName( $index ) {
                $nslist = self::getCanonicalNamespaces();
-               if ( isset( $nslist[$index] ) ) {
-                       return $nslist[$index];
-               } else {
-                       return false;
-               }
+               return $nslist[$index] ?? false;
        }
 
        /**
index 72b1090..bcc3633 100644 (file)
@@ -723,6 +723,9 @@ class MediaWiki {
                        MWExceptionHandler::rollbackMasterChangesAndLog( $e );
                }
 
+               // Disable WebResponse setters for post-send processing (T191537).
+               WebResponse::disableForPostSend();
+
                $blocksHttpClient = true;
                // Defer everything else if possible...
                $callback = function () use ( $mode, &$blocksHttpClient ) {
index 50cc991..0b2ba40 100644 (file)
@@ -755,11 +755,7 @@ class OutputPage extends ContextSource {
         * @return mixed Property value or null if not found
         */
        public function getProperty( $name ) {
-               if ( isset( $this->mProperties[$name] ) ) {
-                       return $this->mProperties[$name];
-               } else {
-                       return null;
-               }
+               return $this->mProperties[$name] ?? null;
        }
 
        /**
@@ -1368,7 +1364,8 @@ class OutputPage extends ContextSource {
                );
 
                # Add the results to the link cache
-               $lb->addResultToCache( LinkCache::singleton(), $res );
+               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
+               $lb->addResultToCache( $linkCache, $res );
 
                return $res;
        }
index 213abbe..b9a03f5 100644 (file)
@@ -1094,8 +1094,6 @@ class Revision implements IDBAccessObject {
 
                // Avoid PHP 7.1 warning of passing $this by reference
                $revision = $this;
-               // TODO: hard-deprecate in 1.32 (or even 1.31?)
-               Hooks::run( 'RevisionInsertComplete', [ &$revision, null, null ] );
 
                return $rec->getId();
        }
index 3f8ba18..379424c 100644 (file)
@@ -471,6 +471,9 @@ return [
                        $blobStore,
                        $services->getMainWANObjectCache(),
                        $services->getCommentStore(),
+                       $services->getContentModelStore(),
+                       $services->getSlotRoleStore(),
+                       $services->getMainConfig()->get( 'MultiContentRevisionSchemaMigrationStage' ),
                        $services->getActorMigration()
                );
 
diff --git a/includes/Storage/DerivedPageDataUpdater.php b/includes/Storage/DerivedPageDataUpdater.php
new file mode 100644 (file)
index 0000000..cc72754
--- /dev/null
@@ -0,0 +1,1542 @@
+<?php
+/**
+ * A handle for managing updates for derived page data on edit, import, purge, etc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use ApiStashEdit;
+use CategoryMembershipChangeJob;
+use Content;
+use ContentHandler;
+use DataUpdate;
+use DeferredUpdates;
+use Hooks;
+use IDBAccessObject;
+use InvalidArgumentException;
+use JobQueueGroup;
+use Language;
+use LinksUpdate;
+use LogicException;
+use MediaWiki\Edit\PreparedEdit;
+use MediaWiki\MediaWikiServices;
+use MediaWiki\User\UserIdentity;
+use MessageCache;
+use ParserCache;
+use ParserOptions;
+use ParserOutput;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+use RecentChangesUpdateJob;
+use ResourceLoaderWikiModule;
+use Revision;
+use SearchUpdate;
+use SiteStatsUpdate;
+use Title;
+use User;
+use Wikimedia\Assert\Assert;
+use WikiPage;
+
+/**
+ * A handle for managing updates for derived page data on edit, import, purge, etc.
+ *
+ * @note Avoid direct usage of DerivedPageDataUpdater.
+ *
+ * @todo Define interfaces for the different use cases of DerivedPageDataUpdater, particularly
+ * providing access to post-PST content and ParserOutput to callbacks during revision creation,
+ * which currently use WikiPage::prepareContentForEdit, and allowing updates to be triggered on
+ * purge, import, and undeletion, which currently use WikiPage::doEditUpdates() and
+ * Content::getSecondaryDataUpdates().
+ *
+ * DerivedPageDataUpdater instances are designed to be cached inside a WikiPage instance,
+ * and re-used by callback code over the course of an update operation. It's a stepping stone
+ * one the way to a more complete refactoring of WikiPage.
+ *
+ * When using a DerivedPageDataUpdater, the following life cycle must be observed:
+ * grabCurrentRevision (optional), prepareContent (optional), prepareUpdate (required
+ * for doUpdates). getCanonicalParserOutput, getSlots, and getSecondaryDataUpdates
+ * require prepareContent or prepareUpdate to have been called first, to initialize the
+ * DerivedPageDataUpdater.
+ *
+ * @see docs/pageupdater.txt for more information.
+ *
+ * MCR migration note: this replaces the relevant methods in WikiPage, and covers the use cases
+ * of PreparedEdit.
+ *
+ * @internal
+ *
+ * @since 1.32
+ * @ingroup Page
+ */
+class DerivedPageDataUpdater implements IDBAccessObject {
+
+       /**
+        * @var UserIdentity|null
+        */
+       private $user = null;
+
+       /**
+        * @var WikiPage
+        */
+       private $wikiPage;
+
+       /**
+        * @var ParserCache
+        */
+       private $parserCache;
+
+       /**
+        * @var RevisionStore
+        */
+       private $revisionStore;
+
+       /**
+        * @var Language
+        */
+       private $contentLanguage;
+
+       /**
+        * @var LoggerInterface
+        */
+       private $saveParseLogger;
+
+       /**
+        * @var JobQueueGroup
+        */
+       private $jobQueueGroup;
+
+       /**
+        * @var MessageCache
+        */
+       private $messageCache;
+
+       /**
+        * @var string see $wgArticleCountMethod
+        */
+       private $articleCountMethod;
+
+       /**
+        * @var boolean see $wgRCWatchCategoryMembership
+        */
+       private $rcWatchCategoryMembership = false;
+
+       /**
+        * See $options on prepareUpdate.
+        */
+       private $options = [
+               'changed' => true,
+               'created' => false,
+               'moved' => false,
+               'restored' => false,
+               'oldcountable' => null,
+               'oldredirect' => null,
+       ];
+
+       /**
+        * The state of the relevant row in page table before the edit.
+        * This is determined by the first call to grabCurrentRevision, prepareContent,
+        * or prepareUpdate.
+        * If pageState was not initialized when prepareUpdate() is called, prepareUpdate() will
+        * attempt to emulate the state of the page table before the edit.
+        *
+        * @var array
+        */
+       private $pageState = null;
+
+       /**
+        * @var RevisionSlotsUpdate|null
+        */
+       private $slotsUpdate = null;
+
+       /**
+        * @var MutableRevisionSlots|null
+        */
+       private $pstContentSlots = null;
+
+       /**
+        * @var object[] anonymous objects with two fields, using slot roles as keys:
+        *  - hasHtml: whether the output contains HTML
+        *  - ParserOutput: the slot's parser output
+        */
+       private $slotsOutput = [];
+
+       /**
+        * @var ParserOutput|null
+        */
+       private $canonicalParserOutput = null;
+
+       /**
+        * @var ParserOptions|null
+        */
+       private $canonicalParserOptions = null;
+
+       /**
+        * @var RevisionRecord
+        */
+       private $revision = null;
+
+       /**
+        * A stage identifier for managing the life cycle of this instance.
+        * Possible stages are 'new', 'knows-current', 'has-content', 'has-revision', and 'done'.
+        *
+        * @see docs/pageupdater.txt for documentation of the life cycle.
+        *
+        * @var string
+        */
+       private $stage = 'new';
+
+       /**
+        * Transition table for managing the life cycle of DerivedPageDateUpdater instances.
+        *
+        * XXX: Overkill. This is a linear order, we could just count. Names are nice though,
+        * and constants are also overkill...
+        *
+        * @see docs/pageupdater.txt for documentation of the life cycle.
+        *
+        * @var array[]
+        */
+       private static $transitions = [
+               'new' => [
+                       'new' => true,
+                       'knows-current' => true,
+                       'has-content' => true,
+                       'has-revision' => true,
+               ],
+               'knows-current' => [
+                       'knows-current' => true,
+                       'has-content' => true,
+                       'has-revision' => true,
+               ],
+               'has-content' => [
+                       'has-content' => true,
+                       'has-revision' => true,
+               ],
+               'has-revision' => [
+                       'has-revision' => true,
+                       'done' => true,
+               ],
+       ];
+
+       /**
+        * @param WikiPage $wikiPage ,
+        * @param RevisionStore $revisionStore
+        * @param ParserCache $parserCache
+        * @param JobQueueGroup $jobQueueGroup
+        * @param MessageCache $messageCache
+        * @param Language $contentLanguage
+        * @param LoggerInterface $saveParseLogger
+        */
+       public function __construct(
+               WikiPage $wikiPage,
+               RevisionStore $revisionStore,
+               ParserCache $parserCache,
+               JobQueueGroup $jobQueueGroup,
+               MessageCache $messageCache,
+               Language $contentLanguage,
+               LoggerInterface $saveParseLogger = null
+       ) {
+               $this->wikiPage = $wikiPage;
+
+               $this->parserCache = $parserCache;
+               $this->revisionStore = $revisionStore;
+               $this->jobQueueGroup = $jobQueueGroup;
+               $this->messageCache = $messageCache;
+               $this->contentLanguage = $contentLanguage;
+
+               // XXX: replace all wfDebug calls with a Logger. Do we nede more than one logger here?
+               $this->saveParseLogger = $saveParseLogger ?: new NullLogger();
+       }
+
+       /**
+        * Transition function for managing the life cycle of this instances.
+        *
+        * @see docs/pageupdater.txt for documentation of the life cycle.
+        *
+        * @param string $newStage the new stage
+        * @return string the previous stage
+        *
+        * @throws LogicException If a transition to the given stage is not possible in the current
+        *         stage.
+        */
+       private function doTransition( $newStage ) {
+               $this->assertTransition( $newStage );
+
+               $oldStage = $this->stage;
+               $this->stage = $newStage;
+
+               return $oldStage;
+       }
+
+       /**
+        * Asserts that a transition to the given stage is possible, without performing it.
+        *
+        * @see docs/pageupdater.txt for documentation of the life cycle.
+        *
+        * @param string $newStage the new stage
+        *
+        * @throws LogicException If this instance is not in the expected stage
+        */
+       private function assertTransition( $newStage ) {
+               if ( empty( self::$transitions[$this->stage][$newStage] ) ) {
+                       throw new LogicException( "Cannot transition from {$this->stage} to $newStage" );
+               }
+       }
+
+       /**
+        * @return bool|string
+        */
+       private function getWikiId() {
+               // TODO: get from RevisionStore
+               return false;
+       }
+
+       /**
+        * Checks whether this DerivedPageDataUpdater can be re-used for running updates targeting
+        * the the given revision.
+        *
+        * @param UserIdentity|null $user The user creating the revision in question
+        * @param RevisionRecord|null $revision New revision (after save, if already saved)
+        * @param RevisionSlotsUpdate|null $slotsUpdate New content (before PST)
+        * @param null|int $parentId Parent revision of the edit (use 0 for page creation)
+        *
+        * @return bool
+        */
+       public function isReusableFor(
+               UserIdentity $user = null,
+               RevisionRecord $revision = null,
+               RevisionSlotsUpdate $slotsUpdate = null,
+               $parentId = null
+       ) {
+               if ( $revision
+                       && $parentId
+                       && $revision->getParentId() !== $parentId
+               ) {
+                       throw new InvalidArgumentException( '$parentId should match the parent of $revision' );
+               }
+
+               if ( $revision
+                       && $user
+                       && $revision->getUser( RevisionRecord::RAW )->getName() !== $user->getName()
+               ) {
+                       throw new InvalidArgumentException( '$user should match the author of $revision' );
+               }
+
+               if ( $user && $this->user && $user->getName() !== $this->user->getName() ) {
+                       return false;
+               }
+
+               if ( $revision && $this->revision && $this->revision->getId() !== $revision->getId() ) {
+                       return false;
+               }
+
+               if ( $revision && !$user ) {
+                       $user = $revision->getUser( RevisionRecord::RAW );
+               }
+
+               if ( $this->pageState
+                       && $revision
+                       && $revision->getParentId() !== null
+                       && $this->pageState['oldId'] !== $revision->getParentId()
+               ) {
+                       return false;
+               }
+
+               if ( $this->pageState
+                       && $parentId !== null
+                       && $this->pageState['oldId'] !== $parentId
+               ) {
+                       return false;
+               }
+
+               if ( $this->revision
+                       && $user
+                       && $this->revision->getUser( RevisionRecord::RAW )->getName() !== $user->getName()
+               ) {
+                       return false;
+               }
+
+               if ( $revision
+                       && $this->user
+                       && $revision->getUser( RevisionRecord::RAW )->getName() !== $this->user->getName()
+               ) {
+                       return false;
+               }
+
+               // NOTE: this check is the primary reason for having the $this->slotsUpdate field!
+               if ( $this->slotsUpdate
+                       && $slotsUpdate
+                       && !$this->slotsUpdate->hasSameUpdates( $slotsUpdate )
+               ) {
+                       return false;
+               }
+
+               if ( $this->pstContentSlots
+                       && $revision
+                       && !$this->pstContentSlots->hasSameContent( $revision->getSlots() )
+               ) {
+                       return false;
+               }
+
+               return true;
+       }
+
+       /**
+        * @param string $articleCountMethod "any" or "link".
+        * @see $wgArticleCountMethod
+        */
+       public function setArticleCountMethod( $articleCountMethod ) {
+               $this->articleCountMethod = $articleCountMethod;
+       }
+
+       /**
+        * @param bool $rcWatchCategoryMembership
+        * @see $wgRCWatchCategoryMembership
+        */
+       public function setRcWatchCategoryMembership( $rcWatchCategoryMembership ) {
+               $this->rcWatchCategoryMembership = $rcWatchCategoryMembership;
+       }
+
+       /**
+        * @return Title
+        */
+       private function getTitle() {
+               // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
+               return $this->wikiPage->getTitle();
+       }
+
+       /**
+        * @return WikiPage
+        */
+       private function getWikiPage() {
+               // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
+               return $this->wikiPage;
+       }
+
+       /**
+        * Determines whether the page being edited already existed.
+        * Only defined after calling grabCurrentRevision() or prepareContent() or prepareUpdate()!
+        *
+        * @return bool
+        * @throws LogicException if called before grabCurrentRevision
+        */
+       public function pageExisted() {
+               $this->assertHasPageState( __METHOD__ );
+
+               return $this->pageState['oldId'] > 0;
+       }
+
+       /**
+        * Returns the revision that was current before the edit. This would be null if the edit
+        * created the page, or the revision's parent for a regular edit, or the revision itself
+        * for a null-edit.
+        * Only defined after calling grabCurrentRevision() or prepareContent() or prepareUpdate()!
+        *
+        * @return RevisionRecord|null the revision that was current before the edit, or null if
+        *         the edit created the page.
+        */
+       private function getOldRevision() {
+               $this->assertHasPageState( __METHOD__ );
+
+               // If 'oldRevision' is not set, load it!
+               // Useful if $this->oldPageState is initialized by prepareUpdate.
+               if ( !array_key_exists( 'oldRevision', $this->pageState ) ) {
+                       /** @var int $oldId */
+                       $oldId = $this->pageState['oldId'];
+                       $flags = $this->useMaster() ? RevisionStore::READ_LATEST : 0;
+                       $this->pageState['oldRevision'] = $oldId
+                               ? $this->revisionStore->getRevisionById( $oldId, $flags )
+                               : null;
+               }
+
+               return $this->pageState['oldRevision'];
+       }
+
+       /**
+        * Returns the revision that was the page's current revision when grabCurrentRevision()
+        * was first called.
+        *
+        * During an edit, that revision will act as the logical parent of the new revision.
+        *
+        * Some updates are performed based on the difference between the database state at the
+        * moment this method is first called, and the state after the edit.
+        *
+        * @see docs/pageupdater.txt for more information on when thie method can and should be called.
+        *
+        * @note After prepareUpdate() was called, grabCurrentRevision() will throw an exception
+        * to avoid confusion, since the page's current revision is then the new revision after
+        * the edit, which was presumably passed to prepareUpdate() as the $revision parameter.
+        * Use getOldRevision() instead to access the revision that used to be current before the
+        * edit.
+        *
+        * @return RevisionRecord|null the page's current revision, or null if the page does not
+        * yet exist.
+        */
+       public function grabCurrentRevision() {
+               if ( $this->pageState ) {
+                       return $this->pageState['oldRevision'];
+               }
+
+               $this->assertTransition( 'knows-current' );
+
+               // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
+               $wikiPage = $this->getWikiPage();
+
+               // Do not call WikiPage::clear(), since the caller may already have caused page data
+               // to be loaded with SELECT FOR UPDATE. Just assert it's loaded now.
+               $wikiPage->loadPageData( self::READ_LATEST );
+               $rev = $wikiPage->getRevision();
+               $current = $rev ? $rev->getRevisionRecord() : null;
+
+               $this->pageState = [
+                       'oldRevision' => $current,
+                       'oldId' => $rev ? $rev->getId() : 0,
+                       'oldIsRedirect' => $wikiPage->isRedirect(), // NOTE: uses page table
+                       'oldCountable' => $wikiPage->isCountable(), // NOTE: uses pagelinks table
+               ];
+
+               $this->doTransition( 'knows-current' );
+
+               return $this->pageState['oldRevision'];
+       }
+
+       /**
+        * Whether prepareUpdate() or prepareContent() have been called on this instance.
+        *
+        * @return bool
+        */
+       public function isContentPrepared() {
+               return $this->pstContentSlots !== null;
+       }
+
+       /**
+        * Whether prepareUpdate() has been called on this instance.
+        *
+        * @return bool
+        */
+       public function isUpdatePrepared() {
+               return $this->revision !== null;
+       }
+
+       /**
+        * @return int
+        */
+       private function getPageId() {
+               // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
+               return $this->wikiPage->getId();
+       }
+
+       /**
+        * @return string
+        */
+       private function getTimestampNow() {
+               // TODO: allow an override to be injected for testing
+               return wfTimestampNow();
+       }
+
+       /**
+        * Whether the content of the target revision is publicly visible.
+        *
+        * @return bool
+        */
+       public function isContentPublic() {
+               if ( $this->revision ) {
+                       // XXX: if that revision is the current revision, this can be skipped
+                       return !$this->revision->isDeleted( RevisionRecord::DELETED_TEXT );
+               } else {
+                       // If the content has not been saved yet, it cannot have been suppressed yet.
+                       return true;
+               }
+       }
+
+       /**
+        * Returns the slot, modified or inherited, after PST, with no audience checks applied.
+        *
+        * @param string $role slot role name
+        *
+        * @throws PageUpdateException If the slot is neither set for update nor inherited from the
+        *        parent revision.
+        * @return SlotRecord
+        */
+       public function getRawSlot( $role ) {
+               return $this->getSlots()->getSlot( $role );
+       }
+
+       /**
+        * Returns the content of the given slot, with no audience checks.
+        *
+        * @throws PageUpdateException If the slot is neither set for update nor inherited from the
+        *        parent revision.
+        * @param string $role slot role name
+        * @return Content
+        */
+       public function getRawContent( $role ) {
+               return $this->getRawSlot( $role )->getContent();
+       }
+
+       /**
+        * Returns the content model of the given slot
+        *
+        * @param string $role slot role name
+        * @return string
+        */
+       private function getContentModel( $role ) {
+               return $this->getRawSlot( $role )->getModel();
+       }
+
+       /**
+        * @param string $role slot role name
+        * @return ContentHandler
+        */
+       private function getContentHandler( $role ) {
+               // TODO: inject something like a ContentHandlerRegistry
+               return ContentHandler::getForModelID( $this->getContentModel( $role ) );
+       }
+
+       private function useMaster() {
+               // TODO: can we just set a flag to true in prepareContent()?
+               return $this->wikiPage->wasLoadedFrom( self::READ_LATEST );
+       }
+
+       /**
+        * @return bool
+        */
+       public function isCountable() {
+               // NOTE: Keep in sync with WikiPage::isCountable.
+
+               if ( !$this->getTitle()->isContentPage() ) {
+                       return false;
+               }
+
+               if ( !$this->isContentPublic() ) {
+                       // This should be irrelevant: countability only applies to the current revision,
+                       // and the current revision is never suppressed.
+                       return false;
+               }
+
+               if ( $this->isRedirect() ) {
+                       return false;
+               }
+
+               $hasLinks = null;
+
+               if ( $this->articleCountMethod === 'link' ) {
+                       $hasLinks = (bool)count( $this->getCanonicalParserOutput()->getLinks() );
+               }
+
+               // TODO: MCR: ask all slots if they have links [SlotHandler/PageTypeHandler]
+               $mainContent = $this->getRawContent( 'main' );
+               return $mainContent->isCountable( $hasLinks );
+       }
+
+       /**
+        * @return bool
+        */
+       public function isRedirect() {
+               // NOTE: main slot determines redirect status
+               $mainContent = $this->getRawContent( 'main' );
+
+               return $mainContent->isRedirect();
+       }
+
+       /**
+        * @param RevisionRecord $rev
+        *
+        * @return bool
+        */
+       private function revisionIsRedirect( RevisionRecord $rev ) {
+               // NOTE: main slot determines redirect status
+               $mainContent = $rev->getContent( 'main', RevisionRecord::RAW );
+
+               return $mainContent->isRedirect();
+       }
+
+       /**
+        * Prepare updates based on an update which has not yet been saved.
+        *
+        * This may be used to create derived data that is needed when creating a new revision;
+        * particularly, this makes available the slots of the new revision via the getSlots()
+        * method, after applying PST and slot inheritance.
+        *
+        * The derived data prepared for revision creation may then later be re-used by doUpdates(),
+        * without the need to re-calculate.
+        *
+        * @see docs/pageupdater.txt for more information on when thie method can and should be called.
+        *
+        * @note: Calling this method more than once with the same $slotsUpdate
+        * has no effect. Calling this method multiple times with different content will cause
+        * an exception.
+        *
+        * @note: Calling this method after prepareUpdate() has been called will cause an exception.
+        *
+        * @param User $user The user to act as context for pre-save transformation (PST).
+        *        Type hint should be reduced to UserIdentity at some point.
+        * @param RevisionSlotsUpdate $slotsUpdate The new content of the slots to be updated
+        *        by this edit, before PST.
+        * @param bool $useStash Whether to use stashed ParserOutput
+        */
+       public function prepareContent(
+               User $user,
+               RevisionSlotsUpdate $slotsUpdate,
+               $useStash = true
+       ) {
+               if ( $this->slotsUpdate ) {
+                       if ( !$this->user ) {
+                               throw new LogicException(
+                                       'Unexpected state: $this->slotsUpdate was initialized, '
+                                       . 'but $this->user was not.'
+                               );
+                       }
+
+                       if ( $this->user->getName() !== $user->getName() ) {
+                               throw new LogicException( 'Can\'t call prepareContent() again for different user! '
+                                       . 'Expected ' . $this->user->getName() . ', got ' . $user->getName()
+                               );
+                       }
+
+                       if ( !$this->slotsUpdate->hasSameUpdates( $slotsUpdate ) ) {
+                               throw new LogicException(
+                                       'Can\'t call prepareContent() again with different slot content!'
+                               );
+                       }
+
+                       return; // prepareContent() already done, nothing to do
+               }
+
+               $this->assertTransition( 'has-content' );
+
+               $wikiPage = $this->getWikiPage(); // TODO: use only for legacy hooks!
+               $title = $this->getTitle();
+
+               $parentRevision = $this->grabCurrentRevision();
+
+               $this->slotsOutput = [];
+               $this->canonicalParserOutput = null;
+               $this->canonicalParserOptions = null;
+
+               // The edit may have already been prepared via api.php?action=stashedit
+               $stashedEdit = false;
+
+               // TODO: MCR: allow output for all slots to be stashed.
+               if ( $useStash && $slotsUpdate->isModifiedSlot( 'main' ) ) {
+                       $mainContent = $slotsUpdate->getModifiedSlot( 'main' )->getContent();
+                       $legacyUser = User::newFromIdentity( $user );
+                       $stashedEdit = ApiStashEdit::checkCache( $title, $mainContent, $legacyUser );
+               }
+
+               if ( $stashedEdit ) {
+                       /** @var ParserOutput $output */
+                       $output = $stashedEdit->output;
+
+                       // TODO: this should happen when stashing the ParserOutput, not now!
+                       $output->setCacheTime( $stashedEdit->timestamp );
+
+                       // TODO: MCR: allow output for all slots to be stashed.
+                       $this->canonicalParserOutput = $output;
+               }
+
+               $userPopts = ParserOptions::newFromUserAndLang( $user, $this->contentLanguage );
+               Hooks::run( 'ArticlePrepareTextForEdit', [ $wikiPage, $userPopts ] );
+
+               $this->user = $user;
+               $this->slotsUpdate = $slotsUpdate;
+
+               if ( $parentRevision ) {
+                       // start out by inheriting all parent slots
+                       $this->pstContentSlots = MutableRevisionSlots::newFromParentRevisionSlots(
+                               $parentRevision->getSlots()->getSlots()
+                       );
+               } else {
+                       $this->pstContentSlots = new MutableRevisionSlots();
+               }
+
+               foreach ( $slotsUpdate->getModifiedRoles() as $role ) {
+                       $slot = $slotsUpdate->getModifiedSlot( $role );
+
+                       if ( $slot->isInherited() ) {
+                               // No PST for inherited slots! Note that "modified" slots may still be inherited
+                               // from an earlier version, e.g. for rollbacks.
+                               $pstSlot = $slot;
+                       } elseif ( $role === 'main' && $stashedEdit ) {
+                               // TODO: MCR: allow PST content for all slots to be stashed.
+                               $pstSlot = SlotRecord::newUnsaved( $role, $stashedEdit->pstContent );
+                       } else {
+                               $content = $slot->getContent();
+                               $pstContent = $content->preSaveTransform( $title, $this->user, $userPopts );
+                               $pstSlot = SlotRecord::newUnsaved( $role, $pstContent );
+                       }
+
+                       $this->pstContentSlots->setSlot( $pstSlot );
+               }
+
+               foreach ( $slotsUpdate->getRemovedRoles() as $role ) {
+                       $this->pstContentSlots->removeSlot( $role );
+               }
+
+               $this->options['created'] = ( $parentRevision === null );
+               $this->options['changed'] = ( $parentRevision === null
+                       || !$this->pstContentSlots->hasSameContent( $parentRevision->getSlots() ) );
+
+               $this->doTransition( 'has-content' );
+       }
+
+       private function assertHasPageState( $method ) {
+               if ( !$this->pageState ) {
+                       throw new LogicException(
+                               'Must call grabCurrentRevision() or prepareContent() '
+                               . 'or prepareUpdate() before calling ' . $method
+                       );
+               }
+       }
+
+       private function assertPrepared( $method ) {
+               if ( !$this->pstContentSlots ) {
+                       throw new LogicException(
+                               'Must call prepareContent() or prepareUpdate() before calling ' . $method
+                       );
+               }
+       }
+
+       /**
+        * Whether the edit creates the page.
+        *
+        * @return bool
+        */
+       public function isCreation() {
+               $this->assertPrepared( __METHOD__ );
+               return $this->options['created'];
+       }
+
+       /**
+        * Whether the edit created, or should create, a new revision (that is, it's not a null-edit).
+        *
+        * @warning: at present, "null-revisions" that do not change content but do have a revision
+        * record would return false after prepareContent(), but true after prepareUpdate()!
+        * This should probably be fixed.
+        *
+        * @return bool
+        */
+       public function isChange() {
+               $this->assertPrepared( __METHOD__ );
+               return $this->options['changed'];
+       }
+
+       /**
+        * Whether the page was a redirect before the edit.
+        *
+        * @return bool
+        */
+       public function wasRedirect() {
+               $this->assertHasPageState( __METHOD__ );
+
+               if ( $this->pageState['oldIsRedirect'] === null ) {
+                       /** @var RevisionRecord $rev */
+                       $rev = $this->pageState['oldRevision'];
+                       if ( $rev ) {
+                               $this->pageState['oldIsRedirect'] = $this->revisionIsRedirect( $rev );
+                       } else {
+                               $this->pageState['oldIsRedirect'] = false;
+                       }
+               }
+
+               return $this->pageState['oldIsRedirect'];
+       }
+
+       /**
+        * Returns the slots of the target revision, after PST.
+        *
+        * @return RevisionSlots
+        */
+       public function getSlots() {
+               $this->assertPrepared( __METHOD__ );
+               return $this->pstContentSlots;
+       }
+
+       /**
+        * Returns the RevisionSlotsUpdate for this updater.
+        *
+        * @return RevisionSlotsUpdate
+        */
+       private function getRevisionSlotsUpdate() {
+               $this->assertPrepared( __METHOD__ );
+
+               if ( !$this->slotsUpdate ) {
+                       if ( !$this->revision ) {
+                               // This should not be possible: if assertPrepared() returns true,
+                               // at least one of $this->slotsUpdate or $this->revision should be set.
+                               throw new LogicException( 'No revision nor a slots update is known!' );
+                       }
+
+                       $old = $this->getOldRevision();
+                       $this->slotsUpdate = RevisionSlotsUpdate::newFromRevisionSlots(
+                               $this->revision->getSlots(),
+                               $old ? $old->getSlots() : null
+                       );
+               }
+               return $this->slotsUpdate;
+       }
+
+       /**
+        * Returns the role names of the slots touched by the new revision,
+        * including removed roles.
+        *
+        * @return string[]
+        */
+       public function getTouchedSlotRoles() {
+               return $this->getRevisionSlotsUpdate()->getTouchedRoles();
+       }
+
+       /**
+        * Returns the role names of the slots modified by the new revision,
+        * not including removed roles.
+        *
+        * @return string[]
+        */
+       public function getModifiedSlotRoles() {
+               return $this->getRevisionSlotsUpdate()->getModifiedRoles();
+       }
+
+       /**
+        * Returns the role names of the slots removed by the new revision.
+        *
+        * @return string[]
+        */
+       public function getRemovedSlotRoles() {
+               return $this->getRevisionSlotsUpdate()->getRemovedRoles();
+       }
+
+       /**
+        * Prepare derived data updates targeting the given Revision.
+        *
+        * Calling this method requires the given revision to be present in the database.
+        * This may be right after a new revision has been created, or when re-generating
+        * derived data e.g. in ApiPurge, RefreshLinksJob, and the refreshLinks
+        * script.
+        *
+        * @see docs/pageupdater.txt for more information on when thie method can and should be called.
+        *
+        * @note: Calling this method more than once with the same revision has no effect.
+        * $options are only used for the first call. Calling this method multiple times with
+        * different revisions will cause an exception.
+        *
+        * @note: If grabCurrentRevision() (or prepareContent()) has been called before
+        * calling this method, $revision->getParentRevision() has to refer to the revision that
+        * was the current revision at the time grabCurrentRevision() was called.
+        *
+        * @param RevisionRecord $revision
+        * @param array $options Array of options, following indexes are used:
+        * - changed: bool, whether the revision changed the content (default true)
+        * - created: bool, whether the revision created the page (default false)
+        * - moved: bool, whether the page was moved (default false)
+        * - restored: bool, whether the page was undeleted (default false)
+        * - oldrevision: Revision object for the pre-update revision (default null)
+        * - parseroutput: The canonical ParserOutput of $revision (default null)
+        * - triggeringuser: The user triggering the update (UserIdentity, default null)
+        * - oldredirect: bool, null, or string 'no-change' (default null):
+        *    - bool: whether the page was counted as a redirect before that
+        *      revision, only used in changed is true and created is false
+        *    - null or 'no-change': don't update the redirect status.
+        * - oldcountable: bool, null, or string 'no-change' (default null):
+        *    - bool: whether the page was counted as an article before that
+        *      revision, only used in changed is true and created is false
+        *    - null: if created is false, don't update the article count; if created
+        *      is true, do update the article count
+        *    - 'no-change': don't update the article count, ever
+        *
+        */
+       public function prepareUpdate( RevisionRecord $revision, array $options = [] ) {
+               Assert::parameter(
+                       !isset( $options['oldrevision'] )
+                       || $options['oldrevision'] instanceof Revision
+                       || $options['oldrevision'] instanceof RevisionRecord,
+                       '$options["oldrevision"]',
+                       'must be a RevisionRecord (or Revision)'
+               );
+               Assert::parameter(
+                       !isset( $options['parseroutput'] )
+                       || $options['parseroutput'] instanceof ParserOutput,
+                       '$options["parseroutput"]',
+                       'must be a ParserOutput'
+               );
+               Assert::parameter(
+                       !isset( $options['triggeringuser'] )
+                       || $options['triggeringuser'] instanceof UserIdentity,
+                       '$options["triggeringuser"]',
+                       'must be a UserIdentity'
+               );
+
+               if ( !$revision->getId() ) {
+                       throw new InvalidArgumentException(
+                               'Revision must have an ID set for it to be used with prepareUpdate()!'
+                       );
+               }
+
+               if ( $this->revision ) {
+                       if ( $this->revision->getId() === $revision->getId() ) {
+                               return; // nothing to do!
+                       } else {
+                               throw new LogicException(
+                                       'Trying to re-use DerivedPageDataUpdater with revision '
+                                       .$revision->getId()
+                                       . ', but it\'s already bound to revision '
+                                       . $this->revision->getId()
+                               );
+                       }
+               }
+
+               if ( $this->pstContentSlots
+                       && !$this->pstContentSlots->hasSameContent( $revision->getSlots() )
+               ) {
+                       throw new LogicException(
+                               'The Revision provided has mismatching content!'
+                       );
+               }
+
+               // Override fields defined in $this->options with values from $options.
+               $this->options = array_intersect_key( $options, $this->options ) + $this->options;
+
+               if ( isset( $this->pageState['oldId'] ) ) {
+                       $oldId = $this->pageState['oldId'];
+               } elseif ( isset( $this->options['oldrevision'] ) ) {
+                       /** @var Revision|RevisionRecord $oldRev */
+                       $oldRev = $this->options['oldrevision'];
+                       $oldId = $oldRev->getId();
+               } else {
+                       $oldId = $revision->getParentId();
+               }
+
+               if ( $oldId !== null ) {
+                       // XXX: what if $options['changed'] disagrees?
+                       // MovePage creates a dummy revision with changed = false!
+                       // We may want to explicitly distinguish between "no new revision" (null-edit)
+                       // and "new revision without new content" (dummy revision).
+
+                       if ( $oldId === $revision->getParentId() ) {
+                               // NOTE: this may still be a NullRevision!
+                               // New revision!
+                               $this->options['changed'] = true;
+                       } elseif ( $oldId === $revision->getId() ) {
+                               // Null-edit!
+                               $this->options['changed'] = false;
+                       } else {
+                               // This indicates that calling code has given us the wrong Revision object
+                               throw new LogicException(
+                                       'The Revision mismatches old revision ID: '
+                                       . 'Old ID is ' . $oldId
+                                       . ', parent ID is ' . $revision->getParentId()
+                                       . ', revision ID is ' . $revision->getId()
+                               );
+                       }
+               }
+
+               // If prepareContent() was used to generate the PST content (which is indicated by
+               // $this->slotsUpdate being set), and this is not a null-edit, then the given
+               // revision must have the acting user as the revision author. Otherwise, user
+               // signatures generated by PST would mismatch the user in the revision record.
+               if ( $this->user !== null && $this->options['changed'] && $this->slotsUpdate ) {
+                       $user = $revision->getUser();
+                       if ( !$this->user->equals( $user ) ) {
+                               throw new LogicException(
+                                       'The Revision provided has a mismatching actor: expected '
+                                       .$this->user->getName()
+                                       . ', got '
+                                       . $user->getName()
+                               );
+                       }
+               }
+
+               // If $this->pageState was not yet initialized by grabCurrentRevision or prepareContent,
+               // emulate the state of the page table before the edit, as good as we can.
+               if ( !$this->pageState ) {
+                       $this->pageState = [
+                               'oldIsRedirect' => isset( $this->options['oldredirect'] )
+                                       && is_bool( $this->options['oldredirect'] )
+                                               ? $this->options['oldredirect']
+                                               : null,
+                               'oldCountable' => isset( $this->options['oldcountable'] )
+                                       && is_bool( $this->options['oldcountable'] )
+                                               ? $this->options['oldcountable']
+                                               : null,
+                       ];
+
+                       if ( $this->options['changed'] ) {
+                               // The edit created a new revision
+                               $this->pageState['oldId'] = $revision->getParentId();
+
+                               if ( isset( $this->options['oldrevision'] ) ) {
+                                       $rev = $this->options['oldrevision'];
+                                       $this->pageState['oldRevision'] = $rev instanceof Revision
+                                               ? $rev->getRevisionRecord()
+                                               : $rev;
+                               }
+                       } else {
+                               // This is a null-edit, so the old revision IS the new revision!
+                               $this->pageState['oldId'] = $revision->getId();
+                               $this->pageState['oldRevision'] = $revision;
+                       }
+               }
+
+               // "created" is forced here
+               $this->options['created'] = ( $this->pageState['oldId'] === 0 );
+
+               $this->revision = $revision;
+               $this->pstContentSlots = $revision->getSlots();
+
+               $this->doTransition( 'has-revision' );
+
+               // NOTE: in case we have a User object, don't override with a UserIdentity.
+               // We already checked that $revision->getUser() mathces $this->user;
+               if ( !$this->user ) {
+                       $this->user = $revision->getUser( RevisionRecord::RAW );
+               }
+
+               // Prune any output that depends on the revision ID.
+               if ( $this->canonicalParserOutput ) {
+                       if ( $this->outputVariesOnRevisionMetaData( $this->canonicalParserOutput, __METHOD__ ) ) {
+                               $this->canonicalParserOutput = null;
+                       }
+               } else {
+                       $this->saveParseLogger->debug( __METHOD__ . ": No prepared canonical output...\n" );
+               }
+
+               if ( $this->slotsOutput ) {
+                       foreach ( $this->slotsOutput as $role => $prep ) {
+                               if ( $this->outputVariesOnRevisionMetaData( $prep->output, __METHOD__ ) ) {
+                                       unset( $this->slotsOutput[$role] );
+                               }
+                       }
+               } else {
+                       $this->saveParseLogger->debug( __METHOD__ . ": No prepared output...\n" );
+               }
+
+               // reset ParserOptions, so the actual revision ID is used in future ParserOutput generation
+               $this->canonicalParserOptions = null;
+
+               // Avoid re-generating the canonical ParserOutput if it's known.
+               // We just trust that the caller is passing the correct ParserOutput!
+               if ( isset( $options['parseroutput'] ) ) {
+                       $this->canonicalParserOutput = $options['parseroutput'];
+               }
+
+               // TODO: optionally get ParserOutput from the ParserCache here.
+               // Move the logic used by RefreshLinksJob here!
+       }
+
+       /**
+        * @param ParserOutput $out
+        * @param string $method
+        * @return bool
+        */
+       private function outputVariesOnRevisionMetaData( ParserOutput $out, $method = __METHOD__ ) {
+               if ( $out->getFlag( 'vary-revision' ) ) {
+                       // XXX: Just keep the output if the speculative revision ID was correct, like below?
+                       $this->saveParseLogger->info(
+                               "$method: Prepared output has vary-revision...\n"
+                       );
+                       return true;
+               } elseif ( $out->getFlag( 'vary-revision-id' )
+                       && $out->getSpeculativeRevIdUsed() !== $this->revision->getId()
+               ) {
+                       $this->saveParseLogger->info(
+                               "$method: Prepared output has vary-revision-id with wrong ID...\n"
+                       );
+                       return true;
+               } elseif ( $out->getFlag( 'vary-user' )
+                       && !$this->options['changed']
+               ) {
+                       // When Alice makes a null-edit on top of Bob's edit,
+                       // {{REVISIONUSER}} must resolve to "Bob", not "Alice", see T135261.
+                       // TODO: to avoid this, we should check for null-edits in makeCanonicalparserOptions,
+                       // and set setCurrentRevisionCallback to return the existing revision when appropriate.
+                       // See also the comment there [dk 2018-05]
+                       $this->saveParseLogger->info(
+                               "$method: Prepared output has vary-user and is null-edit...\n"
+                       );
+                       return true;
+               } else {
+                       wfDebug( "$method: Keeping prepared output...\n" );
+                       return false;
+               }
+       }
+
+       /**
+        * @deprecated This only exists for B/C, use the getters on DerivedPageDataUpdater directly!
+        * @return PreparedEdit
+        */
+       public function getPreparedEdit() {
+               $this->assertPrepared( __METHOD__ );
+
+               $slotsUpdate = $this->getRevisionSlotsUpdate();
+               $preparedEdit = new PreparedEdit();
+
+               $preparedEdit->popts = $this->getCanonicalParserOptions();
+               $preparedEdit->output = $this->getCanonicalParserOutput();
+               $preparedEdit->pstContent = $this->pstContentSlots->getContent( 'main' );
+               $preparedEdit->newContent =
+                       $slotsUpdate->isModifiedSlot( 'main' )
+                       ? $slotsUpdate->getModifiedSlot( 'main' )->getContent()
+                       : $this->pstContentSlots->getContent( 'main' ); // XXX: can we just remove this?
+               $preparedEdit->oldContent = null; // unused. // XXX: could get this from the parent revision
+               $preparedEdit->revid = $this->revision ? $this->revision->getId() : null;
+               $preparedEdit->timestamp = $preparedEdit->output->getCacheTime();
+               $preparedEdit->format = $preparedEdit->pstContent->getDefaultFormat();
+
+               return $preparedEdit;
+       }
+
+       /**
+        * @return bool
+        */
+       private function isContentAccessible() {
+               // XXX: when we move this to a RevisionHtmlProvider, the audience may be configurable!
+               return $this->isContentPublic();
+       }
+
+       /**
+        * @param string $role
+        * @param bool $generateHtml
+        * @return ParserOutput
+        */
+       public function getSlotParserOutput( $role, $generateHtml = true ) {
+               // TODO: factor this out into a RevisionHtmlProvider that can also be used for viewing.
+
+               $this->assertPrepared( __METHOD__ );
+
+               if ( isset( $this->slotsOutput[$role] ) ) {
+                       $entry = $this->slotsOutput[$role];
+
+                       if ( $entry->hasHtml || !$generateHtml ) {
+                               return $entry->output;
+                       }
+               }
+
+               if ( !$this->isContentAccessible() ) {
+                       // empty output
+                       $output = new ParserOutput();
+               } else {
+                       $content = $this->getRawContent( $role );
+
+                       $output = $content->getParserOutput(
+                               $this->getTitle(),
+                               $this->revision ? $this->revision->getId() : null,
+                               $this->getCanonicalParserOptions(),
+                               $generateHtml
+                       );
+               }
+
+               $this->slotsOutput[$role] = (object)[
+                       'output' => $output,
+                       'hasHtml' => $generateHtml,
+               ];
+
+               $output->setCacheTime( $this->getTimestampNow() );
+
+               return $output;
+       }
+
+       /**
+        * @return ParserOutput
+        */
+       public function getCanonicalParserOutput() {
+               if ( $this->canonicalParserOutput ) {
+                       return $this->canonicalParserOutput;
+               }
+
+               // TODO: MCR: logic for combining the output of multiple slot goes here!
+               // TODO: factor this out into a RevisionHtmlProvider that can also be used for viewing.
+               $this->canonicalParserOutput = $this->getSlotParserOutput( 'main' );
+
+               return $this->canonicalParserOutput;
+       }
+
+       /**
+        * @return ParserOptions
+        */
+       public function getCanonicalParserOptions() {
+               if ( $this->canonicalParserOptions ) {
+                       return $this->canonicalParserOptions;
+               }
+
+               // TODO: ParserOptions should *not* be controlled by the ContentHandler!
+               // See T190712 for how to fix this for Wikibase.
+               $this->canonicalParserOptions = $this->wikiPage->makeParserOptions( 'canonical' );
+
+               //TODO: if $this->revision is not set but we already know that we pending update is a
+               // null-edit, we should probably use the page's current revision here.
+               // That would avoid the need for the !$this->options['changed'] branch in
+               // outputVariesOnRevisionMetaData [dk 2018-05]
+
+               if ( $this->revision ) {
+                       // Make sure we use the appropriate revision ID when generating output
+                       $title = $this->getTitle();
+                       $oldCallback = $this->canonicalParserOptions->getCurrentRevisionCallback();
+                       $this->canonicalParserOptions->setCurrentRevisionCallback(
+                               function ( Title $parserTitle, $parser = false ) use ( $title, &$oldCallback ) {
+                                       if ( $parserTitle->equals( $title ) ) {
+                                               $legacyRevision = new Revision( $this->revision );
+                                               return $legacyRevision;
+                                       } else {
+                                               return call_user_func( $oldCallback, $parserTitle, $parser );
+                                       }
+                               }
+                       );
+               } else {
+                       // NOTE: we only get here without READ_LATEST if called directly by application logic
+                       $dbIndex = $this->useMaster()
+                               ? DB_MASTER // use the best possible guess
+                               : DB_REPLICA; // T154554
+
+                       $this->canonicalParserOptions->setSpeculativeRevIdCallback(
+                               function () use ( $dbIndex ) {
+                                       // TODO: inject LoadBalancer!
+                                       $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
+                                       // Use a fresh connection in order to see the latest data, by avoiding
+                                       // stale data from REPEATABLE-READ snapshots.
+                                       // HACK: But don't use a fresh connection in unit tests, since it would not have
+                                       // the fake tables. This should be handled by the LoadBalancer!
+                                       $flags = defined( 'MW_PHPUNIT_TEST' ) ? 0 : $lb::CONN_TRX_AUTO;
+                                       $db = $lb->getConnectionRef( $dbIndex, [], $this->getWikiId(), $flags );
+
+                                       return 1 + (int)$db->selectField(
+                                               'revision',
+                                               'MAX(rev_id)',
+                                               [],
+                                               __METHOD__
+                                       );
+                               }
+                       );
+               }
+
+               return $this->canonicalParserOptions;
+       }
+
+       /**
+        * @param bool $recursive
+        *
+        * @return DataUpdate[]
+        */
+       public function getSecondaryDataUpdates( $recursive = false ) {
+               // TODO: MCR: getSecondaryDataUpdates() needs a complete overhaul to avoid DataUpdates
+               // from different slots overwriting each other in the database. Plan:
+               // * replace direct calls to Content::getSecondaryDataUpdates() with calls to this method
+               // * Construct LinksUpdate here, on the combined ParserOutput, instead of in AbstractContent
+               //   for each slot.
+               // * Pass $slot into getSecondaryDataUpdates() - probably be introducing a new duplicate
+               //   version of this function in ContentHandler.
+               // * The new method gets the PreparedEdit, but no $recursive flag (that's for LinksUpdate)
+               // * Hack: call both the old and the new getSecondaryDataUpdates method here; Pass
+               //   the per-slot ParserOutput to the old method, for B/C.
+               // * Hack: If there is more than one slot, filter LinksUpdate from the DataUpdates
+               //   returned by getSecondaryDataUpdates, and use a LinksUpdated for the combined output
+               //   instead.
+               // * Call the SecondaryDataUpdates hook here (or kill it - its signature doesn't make sense)
+
+               $content = $this->getSlots()->getContent( 'main' );
+
+               // NOTE: $output is the combined output, to be shown in the default view.
+               $output = $this->getCanonicalParserOutput();
+
+               $updates = $content->getSecondaryDataUpdates(
+                       $this->getTitle(), null, $recursive, $output
+               );
+
+               return $updates;
+       }
+
+       /**
+        * Do standard updates after page edit, purge, or import.
+        * Update links tables, site stats, search index, title cache, message cache, etc.
+        * Purges pages that depend on this page when appropriate.
+        * With a 10% chance, triggers pruning the recent changes table.
+        *
+        * @note prepareUpdate() must be called before calling this method!
+        *
+        * MCR migration note: this replaces WikiPage::doEditUpdates.
+        */
+       public function doUpdates() {
+               $this->assertTransition( 'done' );
+
+               // TODO: move logic into a PageEventEmitter service
+
+               $wikiPage = $this->getWikiPage(); // TODO: use only for legacy hooks!
+
+               // NOTE: this may trigger the first parsing of the new content after an edit (when not
+               // using pre-generated stashed output).
+               // XXX: we may want to use the PoolCounter here. This would perhaps allow the initial parse
+               // to be perform post-send. The client could already follow a HTTP redirect to the
+               // page view, but would then have to wait for a response until rendering is complete.
+               $output = $this->getCanonicalParserOutput();
+
+               // Save it to the parser cache.
+               // Make sure the cache time matches page_touched to avoid double parsing.
+               $this->parserCache->save(
+                       $output, $wikiPage, $this->getCanonicalParserOptions(),
+                       $this->revision->getTimestamp(),  $this->revision->getId()
+               );
+
+               $legacyUser = User::newFromIdentity( $this->user );
+               $legacyRevision = new Revision( $this->revision );
+
+               // Update the links tables and other secondary data
+               $recursive = $this->options['changed']; // T52785
+               $updates = $this->getSecondaryDataUpdates( $recursive );
+
+               foreach ( $updates as $update ) {
+                       // TODO: make an $option field for the cause
+                       $update->setCause( 'edit-page', $this->user->getName() );
+                       if ( $update instanceof LinksUpdate ) {
+                               $update->setRevision( $legacyRevision );
+
+                               if ( !empty( $this->options['triggeringuser'] ) ) {
+                                       /** @var UserIdentity|User $triggeringUser */
+                                       $triggeringUser = $this->options['triggeringuser'];
+                                       if ( !$triggeringUser instanceof User ) {
+                                               $triggeringUser = User::newFromIdentity( $triggeringUser );
+                                       }
+
+                                       $update->setTriggeringUser( $triggeringUser );
+                               }
+                       }
+                       DeferredUpdates::addUpdate( $update );
+               }
+
+               // TODO: MCR: check if *any* changed slot supports categories!
+               if ( $this->rcWatchCategoryMembership
+                       && $this->getContentHandler( 'main' )->supportsCategories() === true
+                       && ( $this->options['changed'] || $this->options['created'] )
+                       && !$this->options['restored']
+               ) {
+                       // Note: jobs are pushed after deferred updates, so the job should be able to see
+                       // the recent change entry (also done via deferred updates) and carry over any
+                       // bot/deletion/IP flags, ect.
+                       $this->jobQueueGroup->lazyPush(
+                               new CategoryMembershipChangeJob(
+                                       $this->getTitle(),
+                                       [
+                                               'pageId' => $this->getPageId(),
+                                               'revTimestamp' => $this->revision->getTimestamp(),
+                                       ]
+                               )
+                       );
+               }
+
+               // TODO: replace legacy hook! Use a listener on PageEventEmitter instead!
+               $editInfo = $this->getPreparedEdit();
+               Hooks::run( 'ArticleEditUpdates', [ &$wikiPage, &$editInfo, $this->options['changed'] ] );
+
+               // TODO: replace legacy hook! Use a listener on PageEventEmitter instead!
+               if ( Hooks::run( 'ArticleEditUpdatesDeleteFromRecentchanges', [ &$wikiPage ] ) ) {
+                       // Flush old entries from the `recentchanges` table
+                       if ( mt_rand( 0, 9 ) == 0 ) {
+                               $this->jobQueueGroup->lazyPush( RecentChangesUpdateJob::newPurgeJob() );
+                       }
+               }
+
+               $id = $this->getPageId();
+               $title = $this->getTitle();
+               $dbKey = $title->getPrefixedDBkey();
+               $shortTitle = $title->getDBkey();
+
+               if ( !$title->exists() ) {
+                       wfDebug( __METHOD__ . ": Page doesn't exist any more, bailing out\n" );
+
+                       $this->doTransition( 'done' );
+                       return;
+               }
+
+               if ( $this->options['oldcountable'] === 'no-change' ||
+                       ( !$this->options['changed'] && !$this->options['moved'] )
+               ) {
+                       $good = 0;
+               } elseif ( $this->options['created'] ) {
+                       $good = (int)$this->isCountable();
+               } elseif ( $this->options['oldcountable'] !== null ) {
+                       $good = (int)$this->isCountable()
+                               - (int)$this->options['oldcountable'];
+               } else {
+                       $good = 0;
+               }
+               $edits = $this->options['changed'] ? 1 : 0;
+               $pages = $this->options['created'] ? 1 : 0;
+
+               DeferredUpdates::addUpdate( SiteStatsUpdate::factory(
+                       [ 'edits' => $edits, 'articles' => $good, 'pages' => $pages ]
+               ) );
+
+               // TODO: make search infrastructure aware of slots!
+               $mainSlot = $this->revision->getSlot( 'main' );
+               if ( !$mainSlot->isInherited() && $this->isContentPublic() ) {
+                       DeferredUpdates::addUpdate( new SearchUpdate( $id, $dbKey, $mainSlot->getContent() ) );
+               }
+
+               // If this is another user's talk page, update newtalk.
+               // Don't do this if $options['changed'] = false (null-edits) nor if
+               // it's a minor edit and the user making the edit doesn't generate notifications for those.
+               if ( $this->options['changed']
+                       && $title->getNamespace() == NS_USER_TALK
+                       && $shortTitle != $legacyUser->getTitleKey()
+                       && !( $this->revision->isMinor() && $legacyUser->isAllowed( 'nominornewtalk' ) )
+               ) {
+                       $recipient = User::newFromName( $shortTitle, false );
+                       if ( !$recipient ) {
+                               wfDebug( __METHOD__ . ": invalid username\n" );
+                       } else {
+                               // Allow extensions to prevent user notification
+                               // when a new message is added to their talk page
+                               // TODO: replace legacy hook!  Use a listener on PageEventEmitter instead!
+                               if ( Hooks::run( 'ArticleEditUpdateNewTalk', [ &$wikiPage, $recipient ] ) ) {
+                                       if ( User::isIP( $shortTitle ) ) {
+                                               // An anonymous user
+                                               $recipient->setNewtalk( true, $legacyRevision );
+                                       } elseif ( $recipient->isLoggedIn() ) {
+                                               $recipient->setNewtalk( true, $legacyRevision );
+                                       } else {
+                                               wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" );
+                                       }
+                               }
+                       }
+               }
+
+               if ( $title->getNamespace() == NS_MEDIAWIKI
+                       && $this->getRevisionSlotsUpdate()->isModifiedSlot( 'main' )
+               ) {
+                       $mainContent = $this->isContentPublic() ? $this->getRawContent( 'main' ) : null;
+
+                       $this->messageCache->updateMessageOverride( $title, $mainContent );
+               }
+
+               // TODO: move onArticleCreate and onArticle into a PageEventEmitter service
+               if ( $this->options['created'] ) {
+                       WikiPage::onArticleCreate( $title );
+               } elseif ( $this->options['changed'] ) { // T52785
+                       WikiPage::onArticleEdit( $title, $legacyRevision, $this->getTouchedSlotRoles() );
+               }
+
+               $oldRevision = $this->getOldRevision();
+               $oldLegacyRevision = $oldRevision ? new Revision( $oldRevision ) : null;
+
+               // TODO: In the wiring, register a listener for this on the new PageEventEmitter
+               ResourceLoaderWikiModule::invalidateModuleCache(
+                       $title, $oldLegacyRevision, $legacyRevision, $this->getWikiId()
+               );
+
+               $this->doTransition( 'done' );
+       }
+
+}
index a259ae0..1aa1165 100644 (file)
@@ -44,26 +44,14 @@ class MutableRevisionRecord extends RevisionRecord {
         * the new revision will act as a null-revision.
         *
         * @param RevisionRecord $parent
-        * @param CommentStoreComment $comment
-        * @param UserIdentity $user
-        * @param string $timestamp
         *
         * @return MutableRevisionRecord
         */
-       public static function newFromParentRevision(
-               RevisionRecord $parent,
-               CommentStoreComment $comment,
-               UserIdentity $user,
-               $timestamp
-       ) {
+       public static function newFromParentRevision( RevisionRecord $parent ) {
                // TODO: ideally, we wouldn't need a Title here
                $title = Title::newFromLinkTarget( $parent->getPageAsLinkTarget() );
                $rev = new MutableRevisionRecord( $title, $parent->getWikiId() );
 
-               $rev->setComment( $comment );
-               $rev->setUser( $user );
-               $rev->setTimestamp( $timestamp );
-
                foreach ( $parent->getSlotRoles() as $role ) {
                        $slot = $parent->getSlot( $role, self::RAW );
                        $rev->inheritSlot( $slot );
@@ -140,8 +128,8 @@ class MutableRevisionRecord extends RevisionRecord {
         * @param SlotRecord $parentSlot
         */
        public function inheritSlot( SlotRecord $parentSlot ) {
-               $slot = SlotRecord::newInherited( $parentSlot );
-               $this->setSlot( $slot );
+               $this->mSlots->inheritSlot( $parentSlot );
+               $this->resetAggregateValues();
        }
 
        /**
@@ -180,6 +168,15 @@ class MutableRevisionRecord extends RevisionRecord {
                $this->resetAggregateValues();
        }
 
+       /**
+        * Applies the given update to the slots of this revision.
+        *
+        * @param RevisionSlotsUpdate $update
+        */
+       public function applyUpdate( RevisionSlotsUpdate $update ) {
+               $update->apply( $this->mSlots );
+       }
+
        /**
         * @param CommentStoreComment $comment
         */
index 4cc3730..df94964 100644 (file)
@@ -60,8 +60,6 @@ class MutableRevisionSlots extends RevisionSlots {
         * Sets the given slot.
         * If a slot with the same role is already present, it is replaced.
         *
-        * @note This may cause the slot meta-data for the revision to be lazy-loaded.
-        *
         * @param SlotRecord $slot
         */
        public function setSlot( SlotRecord $slot ) {
@@ -74,10 +72,18 @@ class MutableRevisionSlots extends RevisionSlots {
        }
 
        /**
-        * Sets the content for the slot with the given role.
+        * Sets the given slot to an inherited version of $slot.
         * If a slot with the same role is already present, it is replaced.
         *
-        * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+        * @param SlotRecord $slot
+        */
+       public function inheritSlot( SlotRecord $slot ) {
+               $this->setSlot( SlotRecord::newInherited( $slot ) );
+       }
+
+       /**
+        * Sets the content for the slot with the given role.
+        * If a slot with the same role is already present, it is replaced.
         *
         * @param string $role
         * @param Content $content
@@ -90,8 +96,6 @@ class MutableRevisionSlots extends RevisionSlots {
        /**
         * Remove the slot for the given role, discontinue the corresponding stream.
         *
-        * @note This may cause the slot meta-data for the revision to be lazy-loaded.
-        *
         * @param string $role
         */
        public function removeSlot( $role ) {
index ebce3da..1982d02 100644 (file)
@@ -60,6 +60,8 @@ class NameTableStore {
        private $nameField;
        /** @var null|callable */
        private $normalizationCallback = null;
+       /** @var null|callable */
+       private $insertCallback = null;
 
        /**
         * @param LoadBalancer $dbLoadBalancer A load balancer for acquiring database connections
@@ -71,6 +73,8 @@ class NameTableStore {
         * @param callable $normalizationCallback Normalization to be applied to names before being
         * saved or queried. This should be a callback that accepts and returns a single string.
         * @param bool|string $wikiId The ID of the target wiki database. Use false for the local wiki.
+        * @param callable $insertCallback Callback to change insert fields accordingly.
+        * This parameter was introduced in 1.32
         */
        public function __construct(
                LoadBalancer $dbLoadBalancer,
@@ -80,7 +84,8 @@ class NameTableStore {
                $idField,
                $nameField,
                callable $normalizationCallback = null,
-               $wikiId = false
+               $wikiId = false,
+               callable $insertCallback = null
        ) {
                $this->loadBalancer = $dbLoadBalancer;
                $this->cache = $cache;
@@ -91,6 +96,7 @@ class NameTableStore {
                $this->normalizationCallback = $normalizationCallback;
                $this->wikiId = $wikiId;
                $this->cacheTTL = IExpiringStore::TTL_MONTH;
+               $this->insertCallback = $insertCallback;
        }
 
        /**
@@ -348,7 +354,7 @@ class NameTableStore {
 
                $dbw->insert(
                        $this->table,
-                       [ $this->nameField => $name ],
+                       $this->getFieldsToStore( $name ),
                        __METHOD__,
                        [ 'IGNORE' ]
                );
@@ -363,4 +369,16 @@ class NameTableStore {
                return $dbw->insertId();
        }
 
+       /**
+        * @param string $name
+        * @return array
+        */
+       private function getFieldsToStore( $name ) {
+               $fields = [ $this->nameField => $name ];
+               if ( $this->insertCallback !== null ) {
+                       $fields = call_user_func( $this->insertCallback, $fields );
+               }
+               return $fields;
+       }
+
 }
diff --git a/includes/Storage/PageUpdateException.php b/includes/Storage/PageUpdateException.php
new file mode 100644 (file)
index 0000000..d87374a
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+/**
+ * Exception representing a failure to update a page entry.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use RuntimeException;
+
+/**
+ * Exception representing a failure to update a page entry.
+ *
+ * @since 1.32
+ */
+class PageUpdateException extends RuntimeException {
+
+}
diff --git a/includes/Storage/PageUpdater.php b/includes/Storage/PageUpdater.php
new file mode 100644 (file)
index 0000000..10caac4
--- /dev/null
@@ -0,0 +1,1231 @@
+<?php
+/**
+ * Controller-like object for creating and updating pages by creating new revisions.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ *
+ * @author Daniel Kinzler
+ */
+
+namespace MediaWiki\Storage;
+
+use AtomicSectionUpdate;
+use ChangeTags;
+use CommentStoreComment;
+use Content;
+use ContentHandler;
+use DeferredUpdates;
+use Hooks;
+use InvalidArgumentException;
+use LogicException;
+use ManualLogEntry;
+use MediaWiki\Linker\LinkTarget;
+use MWException;
+use RecentChange;
+use Revision;
+use RuntimeException;
+use Status;
+use Title;
+use User;
+use Wikimedia\Assert\Assert;
+use Wikimedia\Rdbms\DBConnRef;
+use Wikimedia\Rdbms\DBUnexpectedError;
+use Wikimedia\Rdbms\LoadBalancer;
+use WikiPage;
+
+/**
+ * Controller-like object for creating and updating pages by creating new revisions.
+ *
+ * PageUpdater instances provide compare-and-swap (CAS) protection against concurrent updates
+ * between the time grabParentRevision() is called and saveRevision() inserts a new revision.
+ * This allows application logic to safely perform edit conflict resolution using the parent
+ * revision's content.
+ *
+ * @see docs/pageupdater.txt for more information.
+ *
+ * MCR migration note: this replaces the relevant methods in WikiPage.
+ *
+ * @since 1.32
+ * @ingroup Page
+ */
+class PageUpdater {
+
+       /**
+        * @var User
+        */
+       private $user;
+
+       /**
+        * @var WikiPage
+        */
+       private $wikiPage;
+
+       /**
+        * @var DerivedPageDataUpdater
+        */
+       private $derivedDataUpdater;
+
+       /**
+        * @var LoadBalancer
+        */
+       private $loadBalancer;
+
+       /**
+        * @var RevisionStore
+        */
+       private $revisionStore;
+
+       /**
+        * @var boolean see $wgUseAutomaticEditSummaries
+        * @see $wgUseAutomaticEditSummaries
+        */
+       private $useAutomaticEditSummaries = true;
+
+       /**
+        * @var int the RC patrol status the new revision should be marked with.
+        */
+       private $rcPatrolStatus = RecentChange::PRC_UNPATROLLED;
+
+       /**
+        * @var bool whether to create a log entry for new page creations.
+        */
+       private $usePageCreationLog = true;
+
+       /**
+        * @var boolean see $wgAjaxEditStash
+        */
+       private $ajaxEditStash = true;
+
+       /**
+        * The ID of the logical base revision the content of the new revision is based on.
+        * Not to be confused with the immediate parent revision (the current revision before the
+        * new revision is created).
+        * The base revision is the last revision known to the client, while the parent revision
+        * is determined on the server by grabParentRevision().
+        *
+        * @var bool|int
+        */
+       private $baseRevId = false;
+
+       /**
+        * @var array
+        */
+       private $tags = [];
+
+       /**
+        * @var int
+        */
+       private $undidRevId = 0;
+
+       /**
+        * @var RevisionSlotsUpdate
+        */
+       private $slotsUpdate;
+
+       /**
+        * @var Status|null
+        */
+       private $status = null;
+
+       /**
+        * @param User $user
+        * @param WikiPage $wikiPage
+        * @param DerivedPageDataUpdater $derivedDataUpdater
+        * @param LoadBalancer $loadBalancer
+        * @param RevisionStore $revisionStore
+        */
+       public function __construct(
+               User $user,
+               WikiPage $wikiPage,
+               DerivedPageDataUpdater $derivedDataUpdater,
+               LoadBalancer $loadBalancer,
+               RevisionStore $revisionStore
+       ) {
+               $this->user = $user;
+               $this->wikiPage = $wikiPage;
+               $this->derivedDataUpdater = $derivedDataUpdater;
+
+               $this->loadBalancer = $loadBalancer;
+               $this->revisionStore = $revisionStore;
+
+               $this->slotsUpdate = new RevisionSlotsUpdate();
+       }
+
+       /**
+        * Can be used to enable or disable automatic summaries that are applied to certain kinds of
+        * changes, like completely blanking a page.
+        *
+        * @param bool $useAutomaticEditSummaries
+        * @see $wgUseAutomaticEditSummaries
+        */
+       public function setUseAutomaticEditSummaries( $useAutomaticEditSummaries ) {
+               $this->useAutomaticEditSummaries = $useAutomaticEditSummaries;
+       }
+
+       /**
+        * Sets the "patrolled" status of the edit.
+        * Callers should check the "patrol" and "autopatrol" permissions as appropriate.
+        *
+        * @see $wgUseRCPatrol
+        * @see $wgUseNPPatrol
+        *
+        * @param int $status RC patrol status, e.g. RecentChange::PRC_AUTOPATROLLED.
+        */
+       public function setRcPatrolStatus( $status ) {
+               $this->rcPatrolStatus = $status;
+       }
+
+       /**
+        * Whether to create a log entry for new page creations.
+        *
+        * @see $wgPageCreationLog
+        *
+        * @param bool $use
+        */
+       public function setUsePageCreationLog( $use ) {
+               $this->usePageCreationLog = $use;
+       }
+
+       /**
+        * @param bool $ajaxEditStash
+        * @see $wgAjaxEditStash
+        */
+       public function setAjaxEditStash( $ajaxEditStash ) {
+               $this->ajaxEditStash = $ajaxEditStash;
+       }
+
+       private function getWikiId() {
+               return false; // TODO: get from RevisionStore!
+       }
+
+       /**
+        * @param int $mode DB_MASTER or DB_REPLICA
+        *
+        * @return DBConnRef
+        */
+       private function getDBConnectionRef( $mode ) {
+               return $this->loadBalancer->getConnectionRef( $mode, [], $this->getWikiId() );
+       }
+
+       /**
+        * @return LinkTarget
+        */
+       private function getLinkTarget() {
+               // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
+               return $this->wikiPage->getTitle();
+       }
+
+       /**
+        * @return Title
+        */
+       private function getTitle() {
+               // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
+               return $this->wikiPage->getTitle();
+       }
+
+       /**
+        * @return WikiPage
+        */
+       private function getWikiPage() {
+               // NOTE: eventually, we won't get a WikiPage passed into the constructor any more
+               return $this->wikiPage;
+       }
+
+       /**
+        * Checks whether this update conflicts with another update performed since the specified base
+        * revision. A user level "edit conflict" is detected when the base revision known to the client
+        * and specified via setBaseRevisionId() is not the ID of the current revision before the
+        * update. If setBaseRevisionId() was not called, this method always returns false.
+        *
+        * Note that an update expected to be based on a non-existing page will have base revision ID 0,
+        * and is considered to have a conflict if a current revision exists (that is, the page was
+        * created since the base revision was determined by the client).
+        *
+        * This method returning true indicates to calling code that edit conflict resolution should
+        * be applied before saving any data. It does not prevent the update from being performed, and
+        * it should not be confused with a "late" conflict indicated by the "edit-conflict" status.
+        * A "late" conflict is a CAS failure caused by an update being performed concurrently, between
+        * the time grabParentRevision() was called and the time saveRevision() trying to insert the
+        * new revision.
+        *
+        * @note A user level edit conflict is not the same as the "edit-conflict" status triggered by
+        * a CAS failure. Calling this method establishes the CAS token, it does not check against it:
+        * This method calls grabParentRevision(), and thus causes the expected parent revision
+        * for the update to be fixed to the page's current revision at this point in time.
+        * It acts as a compare-and-swap (CAS) token in that it is guaranteed that saveRevision()
+        * will fail with the "edit-conflict" status if the current revision of the page changes after
+        * hasEditConflict() was called and before saveRevision() could insert a new revision.
+        *
+        * @see grabParentRevision()
+        *
+        * @return bool
+        */
+       public function hasEditConflict() {
+               $baseId = $this->getBaseRevisionId();
+               if ( $baseId === false ) {
+                       return false;
+               }
+
+               $parent = $this->grabParentRevision();
+               $parentId = $parent ? $parent->getId() : 0;
+
+               return $parentId !== $baseId;
+       }
+
+       /**
+        * Returns the revision that was the page's current revision when grabParentRevision()
+        * was first called. This revision is the expected parent revision of the update, and will be
+        * recorded as the new revision's parent revision (unless no new revision is created because
+        * the content was not changed).
+        *
+        * This method MUST not be called after saveRevision() was called!
+        *
+        * The current revision determined by the first call to this methods effectively acts a
+        * compare-and-swap (CAS) token which is checked by saveRevision(), which fails if any
+        * concurrent updates created a new revision.
+        *
+        * Application code should call this method before applying transformations to the new
+        * content that depend on the parent revision, e.g. adding/replacing sections, or resolving
+        * conflicts via a 3-way merge. This protects against race conditions triggered by concurrent
+        * updates.
+        *
+        * @see DerivedPageDataUpdater::grabCurrentRevision()
+        *
+        * @note The expected parent revision is not to be confused with the logical base revision.
+        * The base revision is specified by the client, the parent revision is determined from the
+        * database. If base revision and parent revision are not the same, the updates is considered
+        * to require edit conflict resolution.
+        *
+        * @throws LogicException if called after saveRevision().
+        * @return RevisionRecord|null the parent revision, or null of the page does not yet exist.
+        */
+       public function grabParentRevision() {
+               return $this->derivedDataUpdater->grabCurrentRevision();
+       }
+
+       /**
+        * @return string
+        */
+       private function getTimestampNow() {
+               // TODO: allow an override to be injected for testing
+               return wfTimestampNow();
+       }
+
+       /**
+        * Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
+        * This also performs sanity checks against the base revision specified via setBaseRevisionId().
+        *
+        * @param int $flags
+        * @return int Updated $flags
+        */
+       private function checkFlags( $flags ) {
+               if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
+                       if ( $this->baseRevId === false ) {
+                               $flags |= ( $this->derivedDataUpdater->pageExisted() ) ? EDIT_UPDATE : EDIT_NEW;
+                       } else {
+                               $flags |= ( $this->baseRevId > 0 ) ? EDIT_UPDATE : EDIT_NEW;
+                       }
+               }
+
+               return $flags;
+       }
+
+       /**
+        * Set the new content for the given slot role
+        *
+        * @param string $role A slot role name (such as "main")
+        * @param Content $content
+        */
+       public function setContent( $role, Content $content ) {
+               // TODO: MCR: check the role and the content's model against the list of supported
+               // roles, see T194046.
+
+               $this->slotsUpdate->modifyContent( $role, $content );
+       }
+
+       /**
+        * Explicitly inherit a slot from some earlier revision.
+        *
+        * The primary use case for this is rollbacks, when slots are to be inherited from
+        * the rollback target, overriding the content from the parent revision (which is the
+        * revision being rolled back).
+        *
+        * This should typically not be used to inherit slots from the parent revision, which
+        * happens implicitly. Using this method causes the given slot to be treated as "modified"
+        * during revision creation, even if it has the same content as in the parent revision.
+        *
+        * @param SlotRecord $originalSlot A slot already existing in the database, to be inherited
+        *        by the new revision.
+        */
+       public function inheritSlot( SlotRecord $originalSlot ) {
+               // NOTE: this slot is inherited from some other revision, but it's
+               // a "modified" slot for the RevisionSlotsUpdate and DerivedPageDataUpdater,
+               // since it's not implicitly inherited from the parent revision.
+               $inheritedSlot = SlotRecord::newInherited( $originalSlot );
+               $this->slotsUpdate->modifySlot( $inheritedSlot );
+       }
+
+       /**
+        * Removes the slot with the given role.
+        *
+        * This discontinues the "stream" of slots with this role on the page,
+        * preventing the new revision, and any subsequent revisions, from
+        * inheriting the slot with this role.
+        *
+        * @param string $role A slot role name (but not "main")
+        */
+       public function removeSlot( $role ) {
+               if ( $role === 'main' ) {
+                       throw new InvalidArgumentException( 'Cannot remove the main slot!' );
+               }
+
+               $this->slotsUpdate->removeSlot( $role );
+       }
+
+       /**
+        * Returns the ID of the logical base revision of the update. Not to be confused with the
+        * immediate parent revision. The base revision is set via setBaseRevisionId(),
+        * the parent revision is determined by grabParentRevision().
+        *
+        * Application may use this information to detect user level edit conflicts. Edit conflicts
+        * can be resolved by performing a 3-way merge, using the revision returned by this method as
+        * the common base of the conflicting revisions, namely the new revision being saved,
+        * and the revision returned by grabParentRevision().
+        *
+        * @return bool|int The ID of the base revision, 0 if the base is a non-existing page, false
+        *         if no base revision was specified.
+        */
+       public function getBaseRevisionId() {
+               return $this->baseRevId;
+       }
+
+       /**
+        * Sets the ID of the revision the content of this update is based on, if any.
+        * The base revision ID is not to be confused with the new revision's parent revision:
+        * the parent revision is the page's current revision immediately before the new revision
+        * is created; the base revision indicates what revision the client based the content of
+        * the new revision on. If base revision and parent revision are not the same, the update is
+        * considered to require edit conflict resolution.
+        *
+        * @param int|bool $baseRevId The ID of the base revision, or 0 if the update is expected to be
+        *        performed on a non-existing page. false can be used to indicate that the caller
+        *        doesn't care about the base revision.
+        */
+       public function setBaseRevisionId( $baseRevId ) {
+               Assert::parameterType( 'integer|boolean', $baseRevId, '$baseRevId' );
+               $this->baseRevId = $baseRevId;
+       }
+
+       /**
+        * Returns the revision ID set by setUndidRevisionId(), indicating what revision is being
+        * undone by this edit.
+        *
+        * @return int
+        */
+       public function getUndidRevisionId() {
+               return $this->undidRevId;
+       }
+
+       /**
+        * Sets the ID of revision that was undone by the present update.
+        * This is used with the "undo" action, and is expected to hold the oldest revision ID
+        * in case more then one revision is being undone.
+        *
+        * @param int $undidRevId
+        */
+       public function setUndidRevisionId( $undidRevId ) {
+               Assert::parameterType( 'integer', $undidRevId, '$undidRevId' );
+               $this->undidRevId = $undidRevId;
+       }
+
+       /**
+        * Sets a tag to apply to this update.
+        * Callers are responsible for permission checks,
+        * using ChangeTags::canAddTagsAccompanyingChange.
+        * @param string $tag
+        */
+       public function addTag( $tag ) {
+               Assert::parameterType( 'string', $tag, '$tag' );
+               $this->tags[] = trim( $tag );
+       }
+
+       /**
+        * Sets tags to apply to this update.
+        * Callers are responsible for permission checks,
+        * using ChangeTags::canAddTagsAccompanyingChange.
+        * @param string[] $tags
+        */
+       public function addTags( array $tags ) {
+               Assert::parameterElementType( 'string', $tags, '$tags' );
+               foreach ( $tags as $tag ) {
+                       $this->addTag( $tag );
+               }
+       }
+
+       /**
+        * Returns the list of tags set using the addTag() method.
+        *
+        * @return string[]
+        */
+       public function getExplicitTags() {
+               return $this->tags;
+       }
+
+       /**
+        * @param int $flags Bit mask: a bit mask of EDIT_XXX flags.
+        * @return string[]
+        */
+       private function computeEffectiveTags( $flags ) {
+               $tags = $this->tags;
+
+               foreach ( $this->slotsUpdate->getModifiedRoles() as $role ) {
+                       $old_content = $this->getParentContent( $role );
+
+                       $handler = $this->getContentHandler( $role );
+                       $content = $this->slotsUpdate->getModifiedSlot( $role )->getContent();
+
+                       // TODO: MCR: Do this for all slots. Also add tags for removing roles!
+                       $tag = $handler->getChangeTag( $old_content, $content, $flags );
+                       // If there is no applicable tag, null is returned, so we need to check
+                       if ( $tag ) {
+                               $tags[] = $tag;
+                       }
+               }
+
+               // Check for undo tag
+               if ( $this->undidRevId !== 0 && in_array( 'mw-undo', ChangeTags::getSoftwareTags() ) ) {
+                       $tags[] = 'mw-undo';
+               }
+
+               return array_unique( $tags );
+       }
+
+       /**
+        * Returns the content of the given slot of the parent revision, with no audience checks applied.
+        * If there is no parent revision or the slot is not defined, this returns null.
+        *
+        * @param string $role slot role name
+        * @return Content|null
+        */
+       private function getParentContent( $role ) {
+               $parent = $this->grabParentRevision();
+
+               if ( $parent && $parent->hasSlot( $role ) ) {
+                       return $parent->getContent( $role, RevisionRecord::RAW );
+               }
+
+               return null;
+       }
+
+       /**
+        * @param string $role slot role name
+        * @return ContentHandler
+        */
+       private function getContentHandler( $role ) {
+               // TODO: inject something like a ContentHandlerRegistry
+               if ( $this->slotsUpdate->isModifiedSlot( $role ) ) {
+                       $slot = $this->slotsUpdate->getModifiedSlot( $role );
+               } else {
+                       $parent = $this->grabParentRevision();
+
+                       if ( $parent ) {
+                               $slot = $parent->getSlot( $role, RevisionRecord::RAW );
+                       } else {
+                               throw new RevisionAccessException( 'No such slot: ' . $role );
+                       }
+               }
+
+               return ContentHandler::getForModelID( $slot->getModel() );
+       }
+
+       /**
+        * @param int $flags Bit mask: a bit mask of EDIT_XXX flags.
+        *
+        * @return CommentStoreComment
+        */
+       private function makeAutoSummary( $flags ) {
+               if ( !$this->useAutomaticEditSummaries || ( $flags & EDIT_AUTOSUMMARY ) === 0 ) {
+                       return CommentStoreComment::newUnsavedComment( '' );
+               }
+
+               // NOTE: this generates an auto-summary for SOME RANDOM changed slot!
+               // TODO: combine auto-summaries for multiple slots!
+               // XXX: this logic should not be in the storage layer!
+               $roles = $this->slotsUpdate->getModifiedRoles();
+               $role = reset( $roles );
+
+               if ( $role === false ) {
+                       return CommentStoreComment::newUnsavedComment( '' );
+               }
+
+               $handler = $this->getContentHandler( $role );
+               $content = $this->slotsUpdate->getModifiedSlot( $role )->getContent();
+               $old_content = $this->getParentContent( $role );
+               $summary = $handler->getAutosummary( $old_content, $content, $flags );
+
+               return CommentStoreComment::newUnsavedComment( $summary );
+       }
+
+       /**
+        * Change an existing article or create a new article. Updates RC and all necessary caches,
+        * optionally via the deferred update array. This does not check user permissions.
+        *
+        * It is guaranteed that saveRevision() will fail if the current revision of the page
+        * changes after grabParentRevision() was called and before saveRevision() can insert
+        * a new revision, as per the CAS mechanism described above.
+        *
+        * However, the actual parent revision is allowed to be different from the revision set
+        * with setBaseRevisionId(). The caller is responsible for checking this via
+        * hasEditConflict() and adjusting the content of the new revision accordingly,
+        * using a 3-way-merge if desired.
+        *
+        * MCR migration note: this replaces WikiPage::doEditContent. Callers that change to using
+        * saveRevision() now need to check the "minoredit" themselves before using EDIT_MINOR.
+        *
+        * @param CommentStoreComment $summary Edit summary
+        * @param int $flags Bitfield:
+        *      EDIT_NEW
+        *          Create a new page, or fail with "edit-already-exists" if the page exists.
+        *      EDIT_UPDATE
+        *          Create a new revision, or fail with "edit-gone-missing" if the page does not exist.
+        *      EDIT_MINOR
+        *          Mark this revision as minor
+        *      EDIT_SUPPRESS_RC
+        *          Do not log the change in recentchanges
+        *      EDIT_FORCE_BOT
+        *          Mark the revision as automated ("bot edit")
+        *      EDIT_AUTOSUMMARY
+        *          Fill in blank summaries with generated text where possible
+        *      EDIT_INTERNAL
+        *          Signal that the page retrieve/save cycle happened entirely in this request.
+        *
+        * If neither EDIT_NEW nor EDIT_UPDATE is specified, the expected state is detected
+        * automatically via grabParentRevision(). In this case, the "edit-already-exists" or
+        * "edit-gone-missing" errors may still be triggered due to race conditions, if the page
+        * was unexpectedly created or deleted while revision creation is in progress. This can be
+        * viewed as part of the CAS mechanism described above.
+        *
+        * @return RevisionRecord|null The new revision, or null if no new revision was created due
+        *         to a failure or a null-edit. Use isUnchanged(), wasSuccessful() and getStatus()
+        *         to determine the outcome of the revision creation.
+        *
+        * @throws MWException
+        * @throws RuntimeException
+        */
+       public function saveRevision( CommentStoreComment $summary, $flags = 0 ) {
+               // Defend against mistakes caused by differences with the
+               // signature of WikiPage::doEditContent.
+               Assert::parameterType( 'integer', $flags, '$flags' );
+               Assert::parameterType( 'CommentStoreComment', $summary, '$summary' );
+
+               if ( $this->wasCommitted() ) {
+                       throw new RuntimeException( 'saveRevision() has already been called on this PageUpdater!' );
+               }
+
+               // Low-level sanity check
+               if ( $this->getLinkTarget()->getText() === '' ) {
+                       throw new RuntimeException( 'Something is trying to edit an article with an empty title' );
+               }
+
+               // TODO: MCR: check the role and the content's model against the list of supported
+               // and required roles, see T194046.
+
+               // Make sure the given content type is allowed for this page
+               // TODO: decide: Extend check to other slots? Consider the role in check? [PageType]
+               $mainContentHandler = $this->getContentHandler( 'main' );
+               if ( !$mainContentHandler->canBeUsedOn( $this->getTitle() ) ) {
+                       $this->status = Status::newFatal( 'content-not-allowed-here',
+                               ContentHandler::getLocalizedName( $mainContentHandler->getModelID() ),
+                               $this->getTitle()->getPrefixedText()
+                       );
+                       return null;
+               }
+
+               // Load the data from the master database if needed. Needed to check flags.
+               // NOTE: This grabs the parent revision as the CAS token, if grabParentRevision
+               // wasn't called yet. If the page is modified by another process before we are done with
+               // it, this method must fail (with status 'edit-conflict')!
+               // NOTE: The actual parent revision may be different from $this->baseRevisionId.
+               // The caller is responsible for checking this via hasEditConflict and adjusting the
+               // content of the new revision accordingly, using a 3-way-merge.
+               $this->grabParentRevision();
+               $flags = $this->checkFlags( $flags );
+
+               // Avoid statsd noise and wasted cycles check the edit stash (T136678)
+               if ( ( $flags & EDIT_INTERNAL ) || ( $flags & EDIT_FORCE_BOT ) ) {
+                       $useStashed = false;
+               } else {
+                       $useStashed = $this->ajaxEditStash;
+               }
+
+               // TODO: use this only for the legacy hook, and only if something uses the legacy hook
+               $wikiPage = $this->getWikiPage();
+
+               $user = $this->user;
+
+               // Prepare the update. This performs PST and generates the canonical ParserOutput.
+               $this->derivedDataUpdater->prepareContent(
+                       $this->user,
+                       $this->slotsUpdate,
+                       $useStashed
+               );
+
+               // TODO: don't force initialization here!
+               // This is a hack to work around the fact that late initialization of the ParserOutput
+               // causes ApiFlowEditHeaderTest::testCache to fail. Whether that failure indicates an
+               // actual problem, or is just an issue with the test setup, remains to be determined
+               // [dk, 2018-03].
+               // Anomie said in 2018-03:
+               /*
+                       I suspect that what's breaking is this:
+
+                       The old version of WikiPage::doEditContent() called prepareContentForEdit() which
+                       generated the ParserOutput right then, so when doEditUpdates() gets called from the
+                       DeferredUpdate scheduled by WikiPage::doCreate() there's no need to parse. I note
+                       there's a comment there that says "Get the pre-save transform content and final
+                       parser output".
+                       The new version of WikiPage::doEditContent() makes a PageUpdater and calls its
+                       saveRevision(), which calls DerivedPageDataUpdater::prepareContent() and
+                       PageUpdater::doCreate() without ever having to actually generate a ParserOutput.
+                       Thus, when DerivedPageDataUpdater::doUpdates() is called from the DeferredUpdate
+                       scheduled by PageUpdater::doCreate(), it does find that it needs to parse at that point.
+
+                       And the order of operations in that Flow test is presumably:
+
+                       - Create a page with a call to WikiPage::doEditContent(), in a way that somehow avoids
+                       processing the DeferredUpdate.
+                       - Set up the "no set!" mock cache in Flow\Tests\Api\ApiTestCase::expectCacheInvalidate()
+                       - Then, during the course of doing that test, a $db->commit() results in the
+                       DeferredUpdates being run.
+                */
+               $this->derivedDataUpdater->getCanonicalParserOutput();
+
+               $mainContent = $this->derivedDataUpdater->getSlots()->getContent( 'main' );
+
+               // Trigger pre-save hook (using provided edit summary)
+               $hookStatus = Status::newGood( [] );
+               // TODO: replace legacy hook!
+               // TODO: avoid pass-by-reference, see T193950
+               $hook_args = [ &$wikiPage, &$user, &$mainContent, &$summary,
+                       $flags & EDIT_MINOR, null, null, &$flags, &$hookStatus ];
+               // Check if the hook rejected the attempted save
+               if ( !Hooks::run( 'PageContentSave', $hook_args ) ) {
+                       if ( $hookStatus->isOK() ) {
+                               // Hook returned false but didn't call fatal(); use generic message
+                               $hookStatus->fatal( 'edit-hook-aborted' );
+                       }
+
+                       $this->status = $hookStatus;
+                       return null;
+               }
+
+               // Provide autosummaries if one is not provided and autosummaries are enabled
+               // XXX: $summary == null seems logical, but the empty string may actually come from the user
+               // XXX: Move this logic out of the storage layer! It does not belong here! Use a callback?
+               if ( $summary->text === '' && $summary->data === null ) {
+                       $summary = $this->makeAutoSummary( $flags );
+               }
+
+               // Actually create the revision and create/update the page.
+               // Do NOT yet set $this->status!
+               if ( $flags & EDIT_UPDATE ) {
+                       $status = $this->doModify( $summary, $this->user, $flags );
+               } else {
+                       $status = $this->doCreate( $summary, $this->user, $flags );
+               }
+
+               // Promote user to any groups they meet the criteria for
+               DeferredUpdates::addCallableUpdate( function () use ( $user ) {
+                       $user->addAutopromoteOnceGroups( 'onEdit' );
+                       $user->addAutopromoteOnceGroups( 'onView' ); // b/c
+               } );
+
+               // NOTE: set $this->status only after all hooks have been called,
+               // so wasCommitted doesn't return true wehn called indirectly from a hook handler!
+               $this->status = $status;
+
+               // TODO: replace bad status with Exceptions!
+               return ( $this->status && $this->status->isOK() )
+                       ? $this->status->value['revision-record']
+                       : null;
+       }
+
+       /**
+        * Whether saveRevision() has been called on this instance
+        *
+        * @return bool
+        */
+       public function wasCommitted() {
+               return $this->status !== null;
+       }
+
+       /**
+        * The Status object indicating whether saveRevision() was successful, or null if
+        * saveRevision() was not yet called on this instance.
+        *
+        * @note This is here for compatibility with WikiPage::doEditContent. It may be deprecated
+        * soon.
+        *
+        * Possible status errors:
+        *     edit-hook-aborted: The ArticleSave hook aborted the update but didn't
+        *       set the fatal flag of $status.
+        *     edit-gone-missing: In update mode, but the article didn't exist.
+        *     edit-conflict: In update mode, the article changed unexpectedly.
+        *     edit-no-change: Warning that the text was the same as before.
+        *     edit-already-exists: In creation mode, but the article already exists.
+        *
+        *  Extensions may define additional errors.
+        *
+        *  $return->value will contain an associative array with members as follows:
+        *     new: Boolean indicating if the function attempted to create a new article.
+        *     revision: The revision object for the inserted revision, or null.
+        *
+        * @return null|Status
+        */
+       public function getStatus() {
+               return $this->status;
+       }
+
+       /**
+        * Whether saveRevision() completed successfully
+        *
+        * @return bool
+        */
+       public function wasSuccessful() {
+               return $this->status && $this->status->isOK();
+       }
+
+       /**
+        * Whether saveRevision() was called and created a new page.
+        *
+        * @return bool
+        */
+       public function isNew() {
+               return $this->status && $this->status->isOK() && $this->status->value['new'];
+       }
+
+       /**
+        * Whether saveRevision() did not create a revision because the content didn't change
+        * (null-edit). Whether the content changed or not is determined by
+        * DerivedPageDataUpdater::isChange().
+        *
+        * @return bool
+        */
+       public function isUnchanged() {
+               return $this->status
+                       && $this->status->isOK()
+                       && $this->status->value['revision-record'] === null;
+       }
+
+       /**
+        * The new revision created by saveRevision(), or null if saveRevision() has not yet been
+        * called, failed, or did not create a new revision because the content did not change.
+        *
+        * @return RevisionRecord|null
+        */
+       public function getNewRevision() {
+               return ( $this->status && $this->status->isOK() )
+                       ? $this->status->value['revision-record']
+                       : null;
+       }
+
+       /**
+        * Constructs a MutableRevisionRecord based on the Content prepared by the
+        * DerivedPageDataUpdater. This takes care of inheriting slots, updating slots
+        * with PST applied, and removing discontinued slots.
+        *
+        * This calls Content::prepareSave() to verify that the slot content can be saved.
+        * The $status parameter is updated with any errors or warnings found by Content::prepareSave().
+        *
+        * @param CommentStoreComment $comment
+        * @param User $user
+        * @param string $timestamp
+        * @param int $flags
+        * @param Status $status
+        *
+        * @return MutableRevisionRecord
+        */
+       private function makeNewRevision(
+               CommentStoreComment $comment,
+               User $user,
+               $timestamp,
+               $flags,
+               Status $status
+       ) {
+               $wikiPage = $this->getWikiPage();
+               $title = $this->getTitle();
+               $parent = $this->grabParentRevision();
+
+               $rev = new MutableRevisionRecord( $title, $this->getWikiId() );
+               $rev->setPageId( $title->getArticleID() );
+
+               if ( $parent ) {
+                       $oldid = $parent->getId();
+                       $rev->setParentId( $oldid );
+               } else {
+                       $oldid = 0;
+               }
+
+               $rev->setComment( $comment );
+               $rev->setUser( $user );
+               $rev->setTimestamp( $timestamp );
+               $rev->setMinorEdit( ( $flags & EDIT_MINOR ) > 0 );
+
+               foreach ( $this->derivedDataUpdater->getSlots()->getSlots() as $slot ) {
+                       $content = $slot->getContent();
+
+                       // XXX: We may push this up to the "edit controller" level, see T192777.
+                       // TODO: change the signature of PrepareSave to not take a WikiPage!
+                       $prepStatus = $content->prepareSave( $wikiPage, $flags, $oldid, $user );
+
+                       if ( $prepStatus->isOK() ) {
+                               $rev->setSlot( $slot );
+                       }
+
+                       // TODO: MCR: record which problem arose in which slot.
+                       $status->merge( $prepStatus );
+               }
+
+               return $rev;
+       }
+
+       /**
+        * @param CommentStoreComment $summary The edit summary
+        * @param User $user The revision's author
+        * @param int $flags EXIT_XXX constants
+        *
+        * @throws MWException
+        * @return Status
+        */
+       private function doModify( CommentStoreComment $summary, User $user, $flags ) {
+               $wikiPage = $this->getWikiPage(); // TODO: use for legacy hooks only!
+
+               // Update article, but only if changed.
+               $status = Status::newGood( [ 'new' => false, 'revision' => null, 'revision-record' => null ] );
+
+               // Convenience variables
+               $now = $this->getTimestampNow();
+
+               $oldRev = $this->grabParentRevision();
+               $oldid = $oldRev ? $oldRev->getId() : 0;
+
+               if ( !$oldRev ) {
+                       // Article gone missing
+                       $status->fatal( 'edit-gone-missing' );
+
+                       return $status;
+               }
+
+               $newRevisionRecord = $this->makeNewRevision(
+                       $summary,
+                       $user,
+                       $now,
+                       $flags,
+                       $status
+               );
+
+               if ( !$status->isOK() ) {
+                       return $status;
+               }
+
+               // XXX: we may want a flag that allows a null revision to be forced!
+               $changed = $this->derivedDataUpdater->isChange();
+               $mainContent = $newRevisionRecord->getContent( 'main' );
+
+               $dbw = $this->getDBConnectionRef( DB_MASTER );
+
+               if ( $changed ) {
+                       $dbw->startAtomic( __METHOD__ );
+
+                       // Get the latest page_latest value while locking it.
+                       // Do a CAS style check to see if it's the same as when this method
+                       // started. If it changed then bail out before touching the DB.
+                       $latestNow = $wikiPage->lockAndGetLatest(); // TODO: move to storage service, pass DB
+                       if ( $latestNow != $oldid ) {
+                               // We don't need to roll back, since we did not modify the database yet.
+                               // XXX: Or do we want to rollback, any transaction started by calling
+                               // code will fail? If we want that, we should probably throw an exception.
+                               $dbw->endAtomic( __METHOD__ );
+                               // Page updated or deleted in the mean time
+                               $status->fatal( 'edit-conflict' );
+
+                               return $status;
+                       }
+
+                       // At this point we are now comitted to returning an OK
+                       // status unless some DB query error or other exception comes up.
+                       // This way callers don't have to call rollback() if $status is bad
+                       // unless they actually try to catch exceptions (which is rare).
+
+                       // Save revision content and meta-data
+                       $newRevisionRecord = $this->revisionStore->insertRevisionOn( $newRevisionRecord, $dbw );
+                       $newLegacyRevision = new Revision( $newRevisionRecord );
+
+                       // Update page_latest and friends to reflect the new revision
+                       // TODO: move to storage service
+                       $wasRedirect = $this->derivedDataUpdater->wasRedirect();
+                       if ( !$wikiPage->updateRevisionOn( $dbw, $newLegacyRevision, null, $wasRedirect ) ) {
+                               throw new PageUpdateException( "Failed to update page row to use new revision." );
+                       }
+
+                       // TODO: replace legacy hook!
+                       $tags = $this->computeEffectiveTags( $flags );
+                       Hooks::run(
+                               'NewRevisionFromEditComplete',
+                               [ $wikiPage, $newLegacyRevision, $this->baseRevId, $user, &$tags ]
+                       );
+
+                       // Update recentchanges
+                       if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
+                               // Add RC row to the DB
+                               RecentChange::notifyEdit(
+                                       $now,
+                                       $this->getTitle(),
+                                       $newRevisionRecord->isMinor(),
+                                       $user,
+                                       $summary->text, // TODO: pass object when that becomes possible
+                                       $oldid,
+                                       $newRevisionRecord->getTimestamp(),
+                                       ( $flags & EDIT_FORCE_BOT ) > 0,
+                                       '',
+                                       $oldRev->getSize(),
+                                       $newRevisionRecord->getSize(),
+                                       $newRevisionRecord->getId(),
+                                       $this->rcPatrolStatus,
+                                       $tags
+                               );
+                       }
+
+                       $user->incEditCount();
+
+                       $dbw->endAtomic( __METHOD__ );
+               } else {
+                       // T34948: revision ID must be set to page {{REVISIONID}} and
+                       // related variables correctly. Likewise for {{REVISIONUSER}} (T135261).
+                       // Since we don't insert a new revision into the database, the least
+                       // error-prone way is to reuse given old revision.
+                       $newRevisionRecord = $oldRev;
+                       $newLegacyRevision = new Revision( $newRevisionRecord );
+               }
+
+               if ( $changed ) {
+                       // Return the new revision to the caller
+                       $status->value['revision-record'] = $newRevisionRecord;
+
+                       // TODO: globally replace usages of 'revision' with getNewRevision()
+                       $status->value['revision'] = $newLegacyRevision;
+               } else {
+                       $status->warning( 'edit-no-change' );
+                       // Update page_touched as updateRevisionOn() was not called.
+                       // Other cache updates are managed in WikiPage::onArticleEdit()
+                       // via WikiPage::doEditUpdates().
+                       $this->getTitle()->invalidateCache( $now );
+               }
+
+               // Do secondary updates once the main changes have been committed...
+               // NOTE: the updates have to be processed before sending the response to the client
+               // (DeferredUpdates::PRESEND), otherwise the client may already be following the
+               // HTTP redirect to the standard view before dervide data has been created - most
+               // importantly, before the parser cache has been updated. This would cause the
+               // content to be parsed a second time, or may cause stale content to be shown.
+               DeferredUpdates::addUpdate(
+                       new AtomicSectionUpdate(
+                               $dbw,
+                               __METHOD__,
+                               function () use (
+                                       $wikiPage, $newRevisionRecord, $newLegacyRevision, $user, $mainContent,
+                                       $summary, $flags, $changed, $status
+                               ) {
+                                       // Update links tables, site stats, etc.
+                                       $this->derivedDataUpdater->prepareUpdate(
+                                               $newRevisionRecord,
+                                               [
+                                                       'changed' => $changed,
+                                               ]
+                                       );
+                                       $this->derivedDataUpdater->doUpdates();
+
+                                       // Trigger post-save hook
+                                       // TODO: replace legacy hook!
+                                       // TODO: avoid pass-by-reference, see T193950
+                                       $params = [ &$wikiPage, &$user, $mainContent, $summary->text, $flags & EDIT_MINOR,
+                                               null, null, &$flags, $newLegacyRevision, &$status, $this->baseRevId,
+                                               $this->undidRevId ];
+                                       Hooks::run( 'PageContentSaveComplete', $params );
+                               }
+                       ),
+                       DeferredUpdates::PRESEND
+               );
+
+               return $status;
+       }
+
+       /**
+        * @param CommentStoreComment $summary The edit summary
+        * @param User $user The revision's author
+        * @param int $flags EXIT_XXX constants
+        *
+        * @throws DBUnexpectedError
+        * @throws MWException
+        * @return Status
+        */
+       private function doCreate( CommentStoreComment $summary, User $user, $flags ) {
+               $wikiPage = $this->getWikiPage(); // TODO: use for legacy hooks only!
+
+               if ( !$this->derivedDataUpdater->getSlots()->hasSlot( 'main' ) ) {
+                       throw new PageUpdateException( 'Must provide a main slot when creating a page!' );
+               }
+
+               $status = Status::newGood( [ 'new' => true, 'revision' => null, 'revision-record' => null ] );
+
+               $now = $this->getTimestampNow();
+
+               $newRevisionRecord = $this->makeNewRevision(
+                       $summary,
+                       $user,
+                       $now,
+                       $flags,
+                       $status
+               );
+
+               if ( !$status->isOK() ) {
+                       return $status;
+               }
+
+               $dbw = $this->getDBConnectionRef( DB_MASTER );
+               $dbw->startAtomic( __METHOD__ );
+
+               // Add the page record unless one already exists for the title
+               // TODO: move to storage service
+               $newid = $wikiPage->insertOn( $dbw );
+               if ( $newid === false ) {
+                       $dbw->endAtomic( __METHOD__ ); // nothing inserted
+                       $status->fatal( 'edit-already-exists' );
+
+                       return $status; // nothing done
+               }
+
+               // At this point we are now comitted to returning an OK
+               // status unless some DB query error or other exception comes up.
+               // This way callers don't have to call rollback() if $status is bad
+               // unless they actually try to catch exceptions (which is rare).
+               $newRevisionRecord->setPageId( $newid );
+
+               // Save the revision text...
+               $newRevisionRecord = $this->revisionStore->insertRevisionOn( $newRevisionRecord, $dbw );
+               $newLegacyRevision = new Revision( $newRevisionRecord );
+
+               // Update the page record with revision data
+               // TODO: move to storage service
+               if ( !$wikiPage->updateRevisionOn( $dbw, $newLegacyRevision, 0 ) ) {
+                       throw new PageUpdateException( "Failed to update page row to use new revision." );
+               }
+
+               // TODO: replace legacy hook!
+               $tags = $this->computeEffectiveTags( $flags );
+               Hooks::run(
+                       'NewRevisionFromEditComplete',
+                       [ $wikiPage, $newLegacyRevision, false, $user, &$tags ]
+               );
+
+               // Update recentchanges
+               if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
+                       // Add RC row to the DB
+                       RecentChange::notifyNew(
+                               $now,
+                               $this->getTitle(),
+                               $newRevisionRecord->isMinor(),
+                               $user,
+                               $summary->text, // TODO: pass object when that becomes possible
+                               ( $flags & EDIT_FORCE_BOT ) > 0,
+                               '',
+                               $newRevisionRecord->getSize(),
+                               $newRevisionRecord->getId(),
+                               $this->rcPatrolStatus,
+                               $tags
+                       );
+               }
+
+               $user->incEditCount();
+
+               if ( $this->usePageCreationLog ) {
+                       // Log the page creation
+                       // @TODO: Do we want a 'recreate' action?
+                       $logEntry = new ManualLogEntry( 'create', 'create' );
+                       $logEntry->setPerformer( $user );
+                       $logEntry->setTarget( $this->getTitle() );
+                       $logEntry->setComment( $summary->text );
+                       $logEntry->setTimestamp( $now );
+                       $logEntry->setAssociatedRevId( $newRevisionRecord->getId() );
+                       $logEntry->insert();
+                       // Note that we don't publish page creation events to recentchanges
+                       // (i.e. $logEntry->publish()) since this would create duplicate entries,
+                       // one for the edit and one for the page creation.
+               }
+
+               $dbw->endAtomic( __METHOD__ );
+
+               // Return the new revision to the caller
+               // TODO: globally replace usages of 'revision' with getNewRevision()
+               $status->value['revision'] = $newLegacyRevision;
+               $status->value['revision-record'] = $newRevisionRecord;
+
+               // XXX: make sure we are not loading the Content from the DB
+               $mainContent = $newRevisionRecord->getContent( 'main' );
+
+               // Do secondary updates once the main changes have been committed...
+               DeferredUpdates::addUpdate(
+                       new AtomicSectionUpdate(
+                               $dbw,
+                               __METHOD__,
+                               function () use (
+                                       $wikiPage,
+                                       $newRevisionRecord,
+                                       $newLegacyRevision,
+                                       $user,
+                                       $mainContent,
+                                       $summary,
+                                       $flags,
+                                       $status
+                               ) {
+                                       // Update links, etc.
+                                       $this->derivedDataUpdater->prepareUpdate(
+                                               $newRevisionRecord,
+                                               [ 'created' => true ]
+                                       );
+                                       $this->derivedDataUpdater->doUpdates();
+
+                                       // Trigger post-create hook
+                                       // TODO: replace legacy hook!
+                                       // TODO: avoid pass-by-reference, see T193950
+                                       $params = [ &$wikiPage, &$user, $mainContent, $summary->text,
+                                               $flags & EDIT_MINOR, null, null, &$flags, $newLegacyRevision ];
+                                       Hooks::run( 'PageContentInsertComplete', $params );
+                                       // Trigger post-save hook
+                                       // TODO: replace legacy hook!
+                                       $params = array_merge( $params, [ &$status, $this->baseRevId, 0 ] );
+                                       Hooks::run( 'PageContentSaveComplete', $params );
+                               }
+                       ),
+                       DeferredUpdates::PRESEND
+               );
+
+               return $status;
+       }
+
+}
index 66ec2c0..7d1b477 100644 (file)
@@ -218,6 +218,48 @@ abstract class RevisionRecord {
                return $this->mSlots->getSlotRoles();
        }
 
+       /**
+        * Returns the slots defined for this revision.
+        *
+        * @return RevisionSlots
+        */
+       public function getSlots() {
+               return $this->mSlots;
+       }
+
+       /**
+        * Returns the slots that originate in this revision.
+        *
+        * Note that this does not include any slots inherited from some earlier revision,
+        * even if they are different from the slots in the immediate parent revision.
+        * This is the case for rollbacks: slots of a rollback revision are inherited from
+        * the rollback target, and are different from the slots in the parent revision,
+        * which was rolled back.
+        *
+        * To find all slots modified by this revision against its immediate parent
+        * revision, use RevisionSlotsUpdate::newFromRevisionSlots().
+        *
+        * @return RevisionSlots
+        */
+       public function getOriginalSlots() {
+               return new RevisionSlots( $this->mSlots->getOriginalSlots() );
+       }
+
+       /**
+        * Returns slots inherited from some previous revision.
+        *
+        * "Inherited" slots are all slots that do not originate in this revision.
+        * Note that these slots may still differ from the one in the parent revision.
+        * This is the case for rollbacks: slots of a rollback revision are inherited from
+        * the rollback target, and are different from the slots in the parent revision,
+        * which was rolled back.
+        *
+        * @return RevisionSlots
+        */
+       public function getInheritedSlots() {
+               return new RevisionSlots( $this->mSlots->getInheritedSlots() );
+       }
+
        /**
         * Get revision ID. Depending on the concrete subclass, this may return null if
         * the revision ID is not known (e.g. because the revision does not yet exist
index c7dcd13..f37e722 100644 (file)
@@ -202,13 +202,14 @@ class RevisionSlots {
        }
 
        /**
-        * Return all slots that are not inherited.
+        * Return all slots that belong to the revision they originate from (that is,
+        * they are not inherited from some other revision).
         *
         * @note This may cause the slot meta-data for the revision to be lazy-loaded.
         *
         * @return SlotRecord[]
         */
-       public function getTouchedSlots() {
+       public function getOriginalSlots() {
                return array_filter(
                        $this->getSlots(),
                        function ( SlotRecord $slot ) {
@@ -218,7 +219,8 @@ class RevisionSlots {
        }
 
        /**
-        * Return all slots that are inherited.
+        * Return all slots that are not not originate in the revision they belong to (that is,
+        * they are inherited from some other revision).
         *
         * @note This may cause the slot meta-data for the revision to be lazy-loaded.
         *
index 0eef90f..d173a3c 100644 (file)
@@ -72,6 +72,39 @@ class RevisionSlotsUpdate {
                return new RevisionSlotsUpdate( $modified, $removed );
        }
 
+       /**
+        * Constructs a RevisionSlotsUpdate representing the update of $parentSlots
+        * when changing $newContent. If a slot has the same content in $newContent
+        * as in $parentSlots, that slot is considered inherited and thus omitted from
+        * the resulting RevisionSlotsUpdate.
+        *
+        * In contrast to newFromRevisionSlots(), slots in $parentSlots that are not present
+        * in $newContent are not considered removed. They are instead assumed to be inherited.
+        *
+        * @param Content[] $newContent The new content, using slot roles as array keys.
+        *
+        * @return RevisionSlotsUpdate
+        */
+       public static function newFromContent( array $newContent, RevisionSlots $parentSlots = null ) {
+               $modified = [];
+
+               foreach ( $newContent as $role => $content ) {
+                       $slot = SlotRecord::newUnsaved( $role, $content );
+
+                       if ( $parentSlots
+                               && $parentSlots->hasSlot( $role )
+                               && $slot->hasSameContent( $parentSlots->getSlot( $role ) )
+                       ) {
+                               // Skip slots that had the same content in the parent revision from $modified.
+                               continue;
+                       }
+
+                       $modified[$role] = $slot;
+               }
+
+               return new RevisionSlotsUpdate( $modified );
+       }
+
        /**
         * @param SlotRecord[] $modifiedSlots
         * @param string[] $removedRoles
@@ -90,6 +123,11 @@ class RevisionSlotsUpdate {
         * Returns a list of modified slot roles, that is, roles modified by calling modifySlot(),
         * and not later removed by calling removeSlot().
         *
+        * Note that slots in modified roles may still be inherited slots. This is for instance
+        * the case when the RevisionSlotsUpdate objects represents some kind of rollback
+        * operation, in which slots that existed in an earlier revision are restored in
+        * a new revision.
+        *
         * @return string[]
         */
        public function getModifiedRoles() {
@@ -239,4 +277,20 @@ class RevisionSlotsUpdate {
                return true;
        }
 
+       /**
+        * Applies this update to the given MutableRevisionSlots, setting all modified slots,
+        * and removing all removed roles.
+        *
+        * @param MutableRevisionSlots $slots
+        */
+       public function apply( MutableRevisionSlots $slots ) {
+               foreach ( $this->getModifiedRoles() as $role ) {
+                       $slots->setSlot( $this->getModifiedSlot( $role ) );
+               }
+
+               foreach ( $this->getRemovedRoles() as $role ) {
+                       $slots->removeSlot( $role );
+               }
+       }
+
 }
index 36af6cd..6c30d62 100644 (file)
@@ -47,6 +47,7 @@ use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerInterface;
 use Psr\Log\NullLogger;
 use RecentChange;
+use Revision;
 use stdClass;
 use Title;
 use User;
@@ -68,6 +69,8 @@ use Wikimedia\Rdbms\LoadBalancer;
 class RevisionStore
        implements IDBAccessObject, RevisionFactory, RevisionLookup, LoggerAwareInterface {
 
+       const ROW_CACHE_KEY = 'revision-row-1.29';
+
        /**
         * @var SqlBlobStore
         */
@@ -108,6 +111,19 @@ class RevisionStore
         */
        private $logger;
 
+       /**
+        * @var NameTableStore
+        */
+       private $contentModelStore;
+
+       /**
+        * @var NameTableStore
+        */
+       private $slotRoleStore;
+
+       /** @var int One of the MIGRATION_* constants */
+       private $mcrMigrationStage;
+
        /**
         * @todo $blobStore should be allowed to be any BlobStore!
         *
@@ -115,6 +131,9 @@ class RevisionStore
         * @param SqlBlobStore $blobStore
         * @param WANObjectCache $cache
         * @param CommentStore $commentStore
+        * @param NameTableStore $contentModelStore
+        * @param NameTableStore $slotRoleStore
+        * @param int $migrationStage
         * @param ActorMigration $actorMigration
         * @param bool|string $wikiId
         */
@@ -123,15 +142,22 @@ class RevisionStore
                SqlBlobStore $blobStore,
                WANObjectCache $cache,
                CommentStore $commentStore,
+               NameTableStore $contentModelStore,
+               NameTableStore $slotRoleStore,
+               $migrationStage,
                ActorMigration $actorMigration,
                $wikiId = false
        ) {
                Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
+               Assert::parameterType( 'integer', $migrationStage, '$migrationStage' );
 
                $this->loadBalancer = $loadBalancer;
                $this->blobStore = $blobStore;
                $this->cache = $cache;
                $this->commentStore = $commentStore;
+               $this->contentModelStore = $contentModelStore;
+               $this->slotRoleStore = $slotRoleStore;
+               $this->mcrMigrationStage = $migrationStage;
                $this->actorMigration = $actorMigration;
                $this->wikiId = $wikiId;
                $this->logger = new NullLogger();
@@ -157,8 +183,14 @@ class RevisionStore
 
        /**
         * @param bool $contentHandlerUseDB
+        * @throws MWException
         */
        public function setContentHandlerUseDB( $contentHandlerUseDB ) {
+               if ( !$contentHandlerUseDB && $this->mcrMigrationStage > MIGRATION_OLD ) {
+                       throw new MWException(
+                               'Content model must be stored in the database for multi content revision migration.'
+                       );
+               }
                $this->contentHandlerUseDB = $contentHandlerUseDB;
        }
 
@@ -329,14 +361,32 @@ class RevisionStore
                // TODO: pass in a DBTransactionContext instead of a database connection.
                $this->checkDatabaseWikiId( $dbw );
 
-               if ( !$rev->getSlotRoles() ) {
-                       throw new InvalidArgumentException( 'At least one slot needs to be defined!' );
+               $slotRoles = $rev->getSlotRoles();
+
+               // Make sure the main slot is always provided throughout migration
+               if ( !in_array( 'main', $slotRoles ) ) {
+                       throw new InvalidArgumentException(
+                               'main slot must be provided'
+                       );
                }
 
-               if ( $rev->getSlotRoles() !== [ 'main' ] ) {
-                       throw new InvalidArgumentException( 'Only the main slot is supported for now!' );
+               // While inserting into the old schema make sure only the main slot is allowed.
+               // TODO: support extra slots in MIGRATION_WRITE_BOTH mode!
+               if ( $this->mcrMigrationStage <= MIGRATION_WRITE_BOTH && $slotRoles !== [ 'main' ] ) {
+                       throw new InvalidArgumentException(
+                               'Only the main slot is supported with MCR migration mode <= MIGRATION_WRITE_BOTH!'
+                       );
                }
 
+               // Checks
+               $this->failOnNull( $rev->getSize(), 'size field' );
+               $this->failOnEmpty( $rev->getSha1(), 'sha1 field' );
+               $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
+               $comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' );
+               $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
+               $this->failOnNull( $user->getId(), 'user field' );
+               $this->failOnEmpty( $user->getName(), 'user_text field' );
+
                // TODO: we shouldn't need an actual Title here.
                $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
                $pageId = $this->failOnEmpty( $rev->getPageId(), 'rev_page field' ); // check this early
@@ -345,44 +395,145 @@ class RevisionStore
                        ? $this->getPreviousRevisionId( $dbw, $rev )
                        : $rev->getParentId();
 
-               // Record the text (or external storage URL) to the blob store
-               $slot = $rev->getSlot( 'main', RevisionRecord::RAW );
-
-               $size = $this->failOnNull( $rev->getSize(), 'size field' );
-               $sha1 = $this->failOnEmpty( $rev->getSha1(), 'sha1 field' );
-
-               if ( !$slot->hasAddress() ) {
-                       $content = $slot->getContent();
-                       $format = $content->getDefaultFormat();
-                       $model = $content->getModel();
-
-                       $this->checkContentModel( $content, $title );
-
-                       $data = $content->serialize( $format );
-
-                       // Hints allow the blob store to optimize by "leaking" application level information to it.
-                       // TODO: with the new MCR storage schema, we rev_id have this before storing the blobs.
-                       // When we have it, add rev_id as a hint. Can be used with rev_parent_id for
-                       // differential storage or compression of subsequent revisions.
-                       $blobHints = [
-                               BlobStore::DESIGNATION_HINT => 'page-content', // BlobStore may be used for other things too.
-                               BlobStore::PAGE_HINT => $pageId,
-                               BlobStore::ROLE_HINT => $slot->getRole(),
-                               BlobStore::PARENT_HINT => $parentId,
-                               BlobStore::SHA1_HINT => $slot->getSha1(),
-                               BlobStore::MODEL_HINT => $model,
-                               BlobStore::FORMAT_HINT => $format,
-                       ];
+               /** @var RevisionRecord $rev */
+               $rev = $dbw->doAtomicSection(
+                       __METHOD__,
+                       function ( IDatabase $dbw, $fname ) use (
+                               $rev,
+                               $user,
+                               $comment,
+                               $title,
+                               $pageId,
+                               $parentId
+                       ) {
+                               return $this->insertRevisionInternal(
+                                       $rev,
+                                       $dbw,
+                                       $user,
+                                       $comment,
+                                       $title,
+                                       $pageId,
+                                       $parentId
+                               );
+                       }
+               );
 
-                       $blobAddress = $this->blobStore->storeBlob( $data, $blobHints );
-               } else {
-                       $blobAddress = $slot->getAddress();
-                       $model = $slot->getModel();
-                       $format = $slot->getFormat();
+               // sanity checks
+               Assert::postcondition( $rev->getId() > 0, 'revision must have an ID' );
+               Assert::postcondition( $rev->getPageId() > 0, 'revision must have a page ID' );
+               Assert::postcondition(
+                       $rev->getComment( RevisionRecord::RAW ) !== null,
+                       'revision must have a comment'
+               );
+               Assert::postcondition(
+                       $rev->getUser( RevisionRecord::RAW ) !== null,
+                       'revision must have a user'
+               );
+
+               // Trigger exception if the main slot is missing.
+               // Technically, this could go away with MIGRATION_NEW: while
+               // calling code may require a main slot to exist, RevisionStore
+               // really should not know or care about that requirement.
+               $rev->getSlot( 'main', RevisionRecord::RAW );
+
+               foreach ( $slotRoles as $role ) {
+                       $slot = $rev->getSlot( $role, RevisionRecord::RAW );
+                       Assert::postcondition(
+                               $slot->getContent() !== null,
+                               $role .  ' slot must have content'
+                       );
+                       Assert::postcondition(
+                               $slot->hasRevision(),
+                               $role .  ' slot must have a revision associated'
+                       );
                }
 
-               $textId = $this->blobStore->getTextIdFromAddress( $blobAddress );
+               Hooks::run( 'RevisionRecordInserted', [ $rev ] );
 
+               // TODO: deprecate in 1.32!
+               $legacyRevision = new Revision( $rev );
+               Hooks::run( 'RevisionInsertComplete', [ &$legacyRevision, null, null ] );
+
+               return $rev;
+       }
+
+       private function insertRevisionInternal(
+               RevisionRecord $rev,
+               IDatabase $dbw,
+               User $user,
+               CommentStoreComment $comment,
+               Title $title,
+               $pageId,
+               $parentId
+       ) {
+               $slotRoles = $rev->getSlotRoles();
+
+               $revisionRow = $this->insertRevisionRowOn(
+                       $dbw,
+                       $rev,
+                       $title,
+                       $parentId
+               );
+
+               $revisionId = $revisionRow['rev_id'];
+
+               $blobHints = [
+                       BlobStore::PAGE_HINT => $pageId,
+                       BlobStore::REVISION_HINT => $revisionId,
+                       BlobStore::PARENT_HINT => $parentId,
+               ];
+
+               $newSlots = [];
+               foreach ( $slotRoles as $role ) {
+                       $slot = $rev->getSlot( $role, RevisionRecord::RAW );
+
+                       if ( $slot->hasRevision() ) {
+                               // If the SlotRecord already has a revision ID set, this means it already exists
+                               // in the database, and should already belong to the current revision.
+                               // TODO: properly abort transaction if the assertion fails!
+                               Assert::parameter(
+                                       $slot->getRevision() === $revisionId,
+                                       'slot role ' . $slot->getRole(),
+                                       'Existing slot should belong to revision '
+                                       . $revisionId . ', but belongs to revision ' . $slot->getRevision() . '!'
+                               );
+
+                               // Slot exists, nothing to do, move along.
+                               // This happens when restoring archived revisions.
+
+                               $newSlots[$role] = $slot;
+
+                               // Write the main slot's text ID to the revision table for backwards compatibility
+                               if ( $slot->getRole() === 'main' && $this->mcrMigrationStage <= MIGRATION_WRITE_BOTH ) {
+                                       $blobAddress = $slot->getAddress();
+                                       $this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
+                               }
+                       } else {
+                               $newSlots[$role] = $this->insertSlotOn( $dbw, $revisionId, $slot, $title, $blobHints );
+                       }
+               }
+
+               $this->insertIpChangesRow( $dbw, $user, $rev, $revisionId );
+
+               $rev = new RevisionStoreRecord(
+                       $title,
+                       $user,
+                       $comment,
+                       (object)$revisionRow,
+                       new RevisionSlots( $newSlots ),
+                       $this->wikiId
+               );
+
+               return $rev;
+       }
+
+       /**
+        * @param IDatabase $dbw
+        * @param int $revisionId
+        * @param string &$blobAddress (may change!)
+        */
+       private function updateRevisionTextId( IDatabase $dbw, $revisionId, &$blobAddress ) {
+               $textId = $this->blobStore->getTextIdFromAddress( $blobAddress );
                if ( !$textId ) {
                        throw new LogicException(
                                'Blob address not supported in 1.29 database schema: ' . $blobAddress
@@ -393,103 +544,253 @@ class RevisionStore
                // may be a new value, not anything already contained in $blobAddress.
                $blobAddress = SqlBlobStore::makeAddressFromTextId( $textId );
 
-               $comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' );
-               $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
-               $timestamp = $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
-
-               // Checks.
-               $this->failOnNull( $user->getId(), 'user field' );
-               $this->failOnEmpty( $user->getName(), 'user_text field' );
-
-               # Record the edit in revisions
-               $row = [
-                       'rev_page'       => $pageId,
-                       'rev_parent_id'  => $parentId,
-                       'rev_text_id'    => $textId,
-                       'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
-                       'rev_timestamp'  => $dbw->timestamp( $timestamp ),
-                       'rev_deleted'    => $rev->getVisibility(),
-                       'rev_len'        => $size,
-                       'rev_sha1'       => $sha1,
-               ];
+               $dbw->update(
+                       'revision',
+                       [ 'rev_text_id' => $textId ],
+                       [ 'rev_id' => $revisionId ],
+                       __METHOD__
+               );
+       }
 
-               if ( $rev->getId() !== null ) {
-                       // Needed to restore revisions with their original ID
-                       $row['rev_id'] = $rev->getId();
+       /**
+        * @param IDatabase $dbw
+        * @param int $revisionId
+        * @param SlotRecord $protoSlot
+        * @param Title $title
+        * @param array $blobHints See the BlobStore::XXX_HINT constants
+        * @return SlotRecord
+        */
+       private function insertSlotOn(
+               IDatabase $dbw,
+               $revisionId,
+               SlotRecord $protoSlot,
+               Title $title,
+               array $blobHints = []
+       ) {
+               if ( $protoSlot->hasAddress() ) {
+                       $blobAddress = $protoSlot->getAddress();
+               } else {
+                       $blobAddress = $this->storeContentBlob( $protoSlot, $title, $blobHints );
                }
 
-               list( $commentFields, $commentCallback ) =
-                       $this->commentStore->insertWithTempTable( $dbw, 'rev_comment', $comment );
-               $row += $commentFields;
-
-               list( $actorFields, $actorCallback ) =
-                       $this->actorMigration->getInsertValuesWithTempTable( $dbw, 'rev_user', $user );
-               $row += $actorFields;
-
-               if ( $this->contentHandlerUseDB ) {
-                       // MCR migration note: rev_content_model and rev_content_format will go away
+               // Write the main slot's text ID to the revision table for backwards compatibility
+               if ( $protoSlot->getRole() === 'main' && $this->mcrMigrationStage <= MIGRATION_WRITE_BOTH ) {
+                       $this->updateRevisionTextId( $dbw, $revisionId, $blobAddress );
+               }
 
-                       $defaultModel = ContentHandler::getDefaultModelFor( $title );
-                       $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
+               if ( $this->mcrMigrationStage >= MIGRATION_WRITE_BOTH ) {
+                       if ( $protoSlot->hasContentId() ) {
+                               $contentId = $protoSlot->getContentId();
+                       } else {
+                               $contentId = $this->insertContentRowOn( $protoSlot, $dbw, $blobAddress );
+                       }
 
-                       $row['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
-                       $row['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
+                       $this->insertSlotRowOn( $protoSlot, $dbw, $revisionId, $contentId );
+               } else {
+                       $contentId = null;
                }
 
-               $dbw->insert( 'revision', $row, __METHOD__ );
+               $savedSlot = SlotRecord::newSaved(
+                       $revisionId,
+                       $contentId,
+                       $blobAddress,
+                       $protoSlot
+               );
 
-               if ( !isset( $row['rev_id'] ) ) {
-                       // only if auto-increment was used
-                       $row['rev_id'] = intval( $dbw->insertId() );
-               }
-               $commentCallback( $row['rev_id'] );
-               $actorCallback( $row['rev_id'], $row );
+               return $savedSlot;
+       }
 
-               // Insert IP revision into ip_changes for use when querying for a range.
+       /**
+        * Insert IP revision into ip_changes for use when querying for a range.
+        * @param IDatabase $dbw
+        * @param User $user
+        * @param RevisionRecord $rev
+        * @param int $revisionId
+        */
+       private function insertIpChangesRow(
+               IDatabase $dbw,
+               User $user,
+               RevisionRecord $rev,
+               $revisionId
+       ) {
                if ( $user->getId() === 0 && IP::isValid( $user->getName() ) ) {
                        $ipcRow = [
-                               'ipc_rev_id'        => $row['rev_id'],
-                               'ipc_rev_timestamp' => $row['rev_timestamp'],
+                               'ipc_rev_id'        => $revisionId,
+                               'ipc_rev_timestamp' => $dbw->timestamp( $rev->getTimestamp() ),
                                'ipc_hex'           => IP::toHex( $user->getName() ),
                        ];
                        $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
                }
+       }
 
-               $newSlot = SlotRecord::newSaved( $row['rev_id'], $textId, $blobAddress, $slot );
-               $slots = new RevisionSlots( [ 'main' => $newSlot ] );
+       /**
+        * @param IDatabase $dbw
+        * @param RevisionRecord $rev
+        * @param Title $title
+        * @param int $parentId
+        *
+        * @return array a revision table row
+        *
+        * @throws MWException
+        * @throws MWUnknownContentModelException
+        */
+       private function insertRevisionRowOn(
+               IDatabase $dbw,
+               RevisionRecord $rev,
+               Title $title,
+               $parentId
+       ) {
+               $revisionRow = $this->getBaseRevisionRow( $dbw, $rev, $title, $parentId );
 
-               $rev = new RevisionStoreRecord(
-                       $title,
-                       $user,
-                       $comment,
-                       (object)$row,
-                       $slots,
-                       $this->wikiId
-               );
+               list( $commentFields, $commentCallback ) =
+                       $this->commentStore->insertWithTempTable(
+                               $dbw,
+                               'rev_comment',
+                               $rev->getComment( RevisionRecord::RAW )
+                       );
+               $revisionRow += $commentFields;
 
-               $newSlot = $rev->getSlot( 'main', RevisionRecord::RAW );
+               list( $actorFields, $actorCallback ) =
+                       $this->actorMigration->getInsertValuesWithTempTable(
+                               $dbw,
+                               'rev_user',
+                               $rev->getUser( RevisionRecord::RAW )
+                       );
+               $revisionRow += $actorFields;
 
-               // sanity checks
-               Assert::postcondition( $rev->getId() > 0, 'revision must have an ID' );
-               Assert::postcondition( $rev->getPageId() > 0, 'revision must have a page ID' );
-               Assert::postcondition(
-                       $rev->getComment( RevisionRecord::RAW ) !== null,
-                       'revision must have a comment'
-               );
-               Assert::postcondition(
-                       $rev->getUser( RevisionRecord::RAW ) !== null,
-                       'revision must have a user'
-               );
+               $dbw->insert( 'revision', $revisionRow, __METHOD__ );
 
-               Assert::postcondition( $newSlot !== null, 'revision must have a main slot' );
-               Assert::postcondition(
-                       $newSlot->getAddress() !== null,
-                       'main slot must have an addess'
+               if ( !isset( $revisionRow['rev_id'] ) ) {
+                       // only if auto-increment was used
+                       $revisionRow['rev_id'] = intval( $dbw->insertId() );
+               }
+
+               $commentCallback( $revisionRow['rev_id'] );
+               $actorCallback( $revisionRow['rev_id'], $revisionRow );
+
+               return $revisionRow;
+       }
+
+       /**
+        * @param IDatabase $dbw
+        * @param RevisionRecord $rev
+        * @param Title $title
+        * @param int $parentId
+        *
+        * @return array [ 0 => array $revisionRow, 1 => callable  ]
+        * @throws MWException
+        * @throws MWUnknownContentModelException
+        */
+       private function getBaseRevisionRow(
+               IDatabase $dbw,
+               RevisionRecord $rev,
+               Title $title,
+               $parentId
+       ) {
+               // Record the edit in revisions
+               $revisionRow = [
+                       'rev_page'       => $rev->getPageId(),
+                       'rev_parent_id'  => $parentId,
+                       'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
+                       'rev_timestamp'  => $dbw->timestamp( $rev->getTimestamp() ),
+                       'rev_deleted'    => $rev->getVisibility(),
+                       'rev_len'        => $rev->getSize(),
+                       'rev_sha1'       => $rev->getSha1(),
+               ];
+
+               if ( $rev->getId() !== null ) {
+                       // Needed to restore revisions with their original ID
+                       $revisionRow['rev_id'] = $rev->getId();
+               }
+
+               if ( $this->mcrMigrationStage <= MIGRATION_WRITE_BOTH ) {
+                       // In non MCR more this IF section will relate to the main slot
+                       $mainSlot = $rev->getSlot( 'main' );
+                       $model = $mainSlot->getModel();
+                       $format = $mainSlot->getFormat();
+
+                       // MCR migration note: rev_content_model and rev_content_format will go away
+                       if ( $this->contentHandlerUseDB ) {
+                               $defaultModel = ContentHandler::getDefaultModelFor( $title );
+                               $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
+
+                               $revisionRow['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
+                               $revisionRow['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
+                       }
+               }
+
+               return $revisionRow;
+       }
+
+       /**
+        * @param SlotRecord $slot
+        * @param Title $title
+        * @param array $blobHints See the BlobStore::XXX_HINT constants
+        *
+        * @throws MWException
+        * @return string the blob address
+        */
+       private function storeContentBlob(
+               SlotRecord $slot,
+               Title $title,
+               array $blobHints = []
+       ) {
+               $content = $slot->getContent();
+               $format = $content->getDefaultFormat();
+               $model = $content->getModel();
+
+               $this->checkContent( $content, $title );
+
+               return $this->blobStore->storeBlob(
+                       $content->serialize( $format ),
+                       // These hints "leak" some information from the higher abstraction layer to
+                       // low level storage to allow for optimization.
+                       array_merge(
+                               $blobHints,
+                               [
+                                       BlobStore::DESIGNATION_HINT => 'page-content',
+                                       BlobStore::ROLE_HINT => $slot->getRole(),
+                                       BlobStore::SHA1_HINT => $slot->getSha1(),
+                                       BlobStore::MODEL_HINT => $model,
+                                       BlobStore::FORMAT_HINT => $format,
+                               ]
+                       )
                );
+       }
 
-               Hooks::run( 'RevisionRecordInserted', [ $rev ] );
+       /**
+        * @param SlotRecord $slot
+        * @param IDatabase $dbw
+        * @param int $revisionId
+        * @param int $contentId
+        */
+       private function insertSlotRowOn( SlotRecord $slot, IDatabase $dbw, $revisionId, $contentId ) {
+               $slotRow = [
+                       'slot_revision_id' => $revisionId,
+                       'slot_role_id' => $this->slotRoleStore->acquireId( $slot->getRole() ),
+                       'slot_content_id' => $contentId,
+                       // If the slot has a specific origin use that ID, otherwise use the ID of the revision
+                       // that we just inserted.
+                       'slot_origin' => $slot->hasOrigin() ? $slot->getOrigin() : $revisionId,
+               ];
+               $dbw->insert( 'slots', $slotRow, __METHOD__ );
+       }
 
-               return $rev;
+       /**
+        * @param SlotRecord $slot
+        * @param IDatabase $dbw
+        * @param string $blobAddress
+        * @return int content row ID
+        */
+       private function insertContentRowOn( SlotRecord $slot, IDatabase $dbw, $blobAddress ) {
+               $contentRow = [
+                       'content_size' => $slot->getSize(),
+                       'content_sha1' => $slot->getSha1(),
+                       'content_model' => $this->contentModelStore->acquireId( $slot->getModel() ),
+                       'content_address' => $blobAddress,
+               ];
+               $dbw->insert( 'content', $contentRow, __METHOD__ );
+               return intval( $dbw->insertId() );
        }
 
        /**
@@ -501,7 +802,7 @@ class RevisionStore
         * @throws MWException
         * @throws MWUnknownContentModelException
         */
-       private function checkContentModel( Content $content, Title $title ) {
+       private function checkContent( Content $content, Title $title ) {
                // Note: may return null for revisions that have not yet been inserted
 
                $model = $content->getModel();
@@ -552,16 +853,21 @@ class RevisionStore
         * Such revisions can for instance identify page rename
         * operations and other such meta-modifications.
         *
+        * @note: This method grabs a FOR UPDATE lock on the relevant row of the page table,
+        * to prevent a new revision from being inserted before the null revision has been written
+        * to the database.
+        *
         * MCR migration note: this replaces Revision::newNullRevision
         *
         * @todo Introduce newFromParentRevision(). newNullRevision can then be based on that
         * (or go away).
         *
-        * @param IDatabase $dbw
+        * @param IDatabase $dbw used for obtaining the lock on the page table row
         * @param Title $title Title of the page to read from
         * @param CommentStoreComment $comment RevisionRecord's summary
         * @param bool $minor Whether the revision should be considered as minor
         * @param User $user The user to attribute the revision to
+        *
         * @return RevisionRecord|null RevisionRecord or null on error
         */
        public function newNullRevision(
@@ -573,56 +879,34 @@ class RevisionStore
        ) {
                $this->checkDatabaseWikiId( $dbw );
 
-               $fields = [ 'page_latest', 'page_namespace', 'page_title',
-                       'rev_id', 'rev_text_id', 'rev_len', 'rev_sha1' ];
-
-               if ( $this->contentHandlerUseDB ) {
-                       $fields[] = 'rev_content_model';
-                       $fields[] = 'rev_content_format';
-               }
-
-               $current = $dbw->selectRow(
-                       [ 'page', 'revision' ],
-                       $fields,
-                       [
-                               'page_id' => $title->getArticleID(),
-                               'page_latest=rev_id',
-                       ],
+               // T51581: Lock the page table row to ensure no other process
+               // is adding a revision to the page at the same time.
+               // Avoid locking extra tables, compare T191892.
+               $pageLatest = $dbw->selectField(
+                       'page',
+                       'page_latest',
+                       [ 'page_id' => $title->getArticleID() ],
                        __METHOD__,
-                       [ 'FOR UPDATE' ] // T51581
+                       [ 'FOR UPDATE' ]
                );
 
-               if ( $current ) {
-                       $fields = [
-                               'page'        => $title->getArticleID(),
-                               'user_text'   => $user->getName(),
-                               'user'        => $user->getId(),
-                               'actor'       => $user->getActorId(),
-                               'comment'     => $comment,
-                               'minor_edit'  => $minor,
-                               'text_id'     => $current->rev_text_id,
-                               'parent_id'   => $current->page_latest,
-                               'slot_origin' => $current->page_latest,
-                               'len'         => $current->rev_len,
-                               'sha1'        => $current->rev_sha1
-                       ];
+               if ( !$pageLatest ) {
+                       return null;
+               }
 
-                       if ( $this->contentHandlerUseDB ) {
-                               $fields['content_model'] = $current->rev_content_model;
-                               $fields['content_format'] = $current->rev_content_format;
-                       }
+               // Fetch the actual revision row, without locking all extra tables.
+               $oldRevision = $this->loadRevisionFromId( $dbw, $pageLatest );
 
-                       $fields['title'] = Title::makeTitle( $current->page_namespace, $current->page_title );
+               // Construct the new revision
+               $timestamp = wfTimestampNow(); // TODO: use a callback, so we can override it for testing.
+               $newRevision = MutableRevisionRecord::newFromParentRevision( $oldRevision );
 
-                       $mainSlot = $this->emulateMainSlot_1_29( $fields, self::READ_LATEST, $title );
-                       $revision = new MutableRevisionRecord( $title, $this->wikiId );
-                       $this->initializeMutableRevisionFromArray( $revision, $fields );
-                       $revision->setSlot( $mainSlot );
-               } else {
-                       $revision = null;
-               }
+               $newRevision->setComment( $comment );
+               $newRevision->setUser( $user );
+               $newRevision->setTimestamp( $timestamp );
+               $newRevision->setMinorEdit( $minor );
 
-               return $revision;
+               return $newRevision;
        }
 
        /**
@@ -750,20 +1034,24 @@ class RevisionStore
                $mainSlotRow->model_name = null;
                $mainSlotRow->slot_revision_id = null;
                $mainSlotRow->content_address = null;
-               $mainSlotRow->slot_content_id = null;
 
                $content = null;
                $blobData = null;
                $blobFlags = null;
 
                if ( is_object( $row ) ) {
+                       if ( $this->mcrMigrationStage >= MIGRATION_NEW ) {
+                               // Don't emulate from a row when using the new schema.
+                               // Emulating from an array is still OK.
+                               throw new LogicException( 'Can\'t emulate the main slot when using MCR schema.' );
+                       }
+
                        // archive row
                        if ( !isset( $row->rev_id ) && ( isset( $row->ar_user ) || isset( $row->ar_actor ) ) ) {
                                $row = $this->mapArchiveFields( $row );
                        }
 
                        if ( isset( $row->rev_text_id ) && $row->rev_text_id > 0 ) {
-                               $mainSlotRow->slot_content_id = $row->rev_text_id;
                                $mainSlotRow->content_address = SqlBlobStore::makeAddressFromTextId(
                                        $row->rev_text_id
                                );
@@ -798,9 +1086,6 @@ class RevisionStore
                } elseif ( is_array( $row ) ) {
                        $mainSlotRow->slot_revision_id = isset( $row['id'] ) ? intval( $row['id'] ) : null;
 
-                       $mainSlotRow->slot_content_id = isset( $row['text_id'] )
-                               ? intval( $row['text_id'] )
-                               : null;
                        $mainSlotRow->slot_origin = isset( $row['slot_origin'] )
                                ? intval( $row['slot_origin'] )
                                : null;
@@ -856,6 +1141,9 @@ class RevisionStore
                }
 
                if ( !$content ) {
+                       // XXX: We should perhaps fail if $blobData is null and $mainSlotRow->content_address
+                       // is missing, but "empty revisions" with no content are used in some edge cases.
+
                        $content = function ( SlotRecord $slot )
                                use ( $blobData, $blobFlags, $queryFlags, $mainSlotRow )
                        {
@@ -869,7 +1157,16 @@ class RevisionStore
                        };
                }
 
-               $mainSlotRow->slot_id = $mainSlotRow->slot_revision_id;
+               // NOTE: this callback will be looped through RevisionSlot::newInherited(), allowing
+               // the inherited slot to have the same content_id as the original slot. In that case,
+               // $slot will be the inherited slot, while $mainSlotRow still refers to the original slot.
+               $mainSlotRow->slot_content_id =
+                       function ( SlotRecord $slot ) use ( $queryFlags, $mainSlotRow ) {
+                               list( $dbMode, ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
+                               $db = $this->getDBConnectionRef( $dbMode );
+                               return $this->findSlotContentId( $db, $mainSlotRow->slot_revision_id, 'main' );
+                       };
+
                return new SlotRecord( $mainSlotRow, $content );
        }
 
@@ -1064,6 +1361,86 @@ class RevisionStore
                );
        }
 
+       /**
+        * @param int $revId The revision to load slots for.
+        * @param int $queryFlags
+        *
+        * @return SlotRecord[]
+        */
+       private function loadSlotRecords( $revId, $queryFlags ) {
+               $revQuery = self::getSlotsQueryInfo( [ 'content' ] );
+
+               list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
+               $db = $this->getDBConnectionRef( $dbMode );
+
+               $res = $db->select(
+                       $revQuery['tables'],
+                       $revQuery['fields'],
+                       [
+                               'slot_revision_id' => $revId,
+                       ],
+                       __METHOD__,
+                       $dbOptions,
+                       $revQuery['joins']
+               );
+
+               $slots = [];
+
+               foreach ( $res as $row ) {
+                       $contentCallback = function ( SlotRecord $slot ) use ( $queryFlags, $row ) {
+                               return $this->loadSlotContent( $slot, null, null, null, $queryFlags );
+                       };
+
+                       $slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
+               }
+
+               if ( !isset( $slots['main'] ) ) {
+                       throw new RevisionAccessException(
+                               'Main slot of revision ' . $revId . ' not found in database!'
+                       );
+               };
+
+               return $slots;
+       }
+
+       /**
+        * Factory method for RevisionSlots.
+        *
+        * @note If other code has a need to construct RevisionSlots objects, this should be made
+        * public, since RevisionSlots instances should not be constructed directly.
+        *
+        * @param int $revId
+        * @param object $revisionRow
+        * @param int $queryFlags
+        * @param Title $title
+        *
+        * @return RevisionSlots
+        * @throws MWException
+        */
+       private function newRevisionSlots(
+               $revId,
+               $revisionRow,
+               $queryFlags,
+               Title $title
+       ) {
+               if ( $this->mcrMigrationStage < MIGRATION_NEW ) {
+                       // TODO: in MIGRATION_WRITE_BOTH, we could use the old and the new method:
+                       // e.g. call emulateMainSlot_1_29() if loadSlotRecords() fails.
+
+                       $mainSlot = $this->emulateMainSlot_1_29( $revisionRow, $queryFlags, $title );
+                       $slots = new RevisionSlots( [ 'main' => $mainSlot ] );
+               } else {
+                       // XXX: do we need the same kind of caching here
+                       // that getKnownCurrentRevision uses (if $revId == page_latest?)
+
+                       $slots = new RevisionSlots( function () use( $revId, $queryFlags ) {
+                               return $this->loadSlotRecords( $revId, $queryFlags );
+                       } );
+               }
+
+               return $slots;
+       }
+
        /**
         * Make a fake revision object from an archive table row. This is queried
         * for permissions or even inserted (as in Special:Undelete)
@@ -1130,14 +1507,13 @@ class RevisionStore
                        // Legacy because $row may have come from self::selectFields()
                        ->getCommentLegacy( $this->getDBConnection( DB_REPLICA ), 'ar_comment', $row, true );
 
-               $mainSlot = $this->emulateMainSlot_1_29( $row, $queryFlags, $title );
-               $slots = new RevisionSlots( [ 'main' => $mainSlot ] );
+               $slots = $this->newRevisionSlots( $row->ar_rev_id, $row, $queryFlags, $title );
 
                return new RevisionArchiveRecord( $title, $user, $comment, $row, $slots, $this->wikiId );
        }
 
        /**
-        * @see RevisionFactory::newRevisionFromRow_1_29
+        * @see RevisionFactory::newRevisionFromRow
         *
         * MCR migration note: this replaces Revision::newFromRow
         *
@@ -1146,10 +1522,8 @@ class RevisionStore
         * @param Title|null $title
         *
         * @return RevisionRecord
-        * @throws MWException
-        * @throws RevisionAccessException
         */
-       private function newRevisionFromRow_1_29( $row, $queryFlags = 0, Title $title = null ) {
+       public function newRevisionFromRow( $row, $queryFlags = 0, Title $title = null ) {
                Assert::parameterType( 'object', $row, '$row' );
 
                if ( !$title ) {
@@ -1181,27 +1555,11 @@ class RevisionStore
                        // Legacy because $row may have come from self::selectFields()
                        ->getCommentLegacy( $this->getDBConnection( DB_REPLICA ), 'rev_comment', $row, true );
 
-               $mainSlot = $this->emulateMainSlot_1_29( $row, $queryFlags, $title );
-               $slots = new RevisionSlots( [ 'main' => $mainSlot ] );
+               $slots = $this->newRevisionSlots( $row->rev_id, $row, $queryFlags, $title );
 
                return new RevisionStoreRecord( $title, $user, $comment, $row, $slots, $this->wikiId );
        }
 
-       /**
-        * @see RevisionFactory::newRevisionFromRow
-        *
-        * MCR migration note: this replaces Revision::newFromRow
-        *
-        * @param object $row
-        * @param int $queryFlags
-        * @param Title|null $title
-        *
-        * @return RevisionRecord
-        */
-       public function newRevisionFromRow( $row, $queryFlags = 0, Title $title = null ) {
-               return $this->newRevisionFromRow_1_29( $row, $queryFlags, $title );
-       }
-
        /**
         * Constructs a new MutableRevisionRecord based on the given associative array following
         * the MW1.29 convention for the Revision constructor.
@@ -1242,14 +1600,22 @@ class RevisionStore
 
                // if we have a content object, use it to set the model and type
                if ( !empty( $fields['content'] ) ) {
-                       if ( !( $fields['content'] instanceof Content ) ) {
-                               throw new MWException( 'content field must contain a Content object.' );
+                       if ( !( $fields['content'] instanceof Content ) && !is_array( $fields['content'] ) ) {
+                               throw new MWException(
+                                       'content field must contain a Content object or an array of Content objects.'
+                               );
                        }
+               }
 
-                       if ( !empty( $fields['text_id'] ) ) {
+               if ( !empty( $fields['text_id'] ) ) {
+                       if ( $this->mcrMigrationStage >= MIGRATION_NEW ) {
+                               throw new MWException( "Cannot use text_id field with MCR schema" );
+                       }
+
+                       if ( !empty( $fields['content'] ) ) {
                                throw new MWException(
                                        "Text already stored in external store (id {$fields['text_id']}), " .
-                                       "can't serialize content object"
+                                       "can't specify content object"
                                );
                        }
                }
@@ -1274,11 +1640,17 @@ class RevisionStore
                        }
                }
 
-               $mainSlot = $this->emulateMainSlot_1_29( $fields, $queryFlags, $title );
-
                $revision = new MutableRevisionRecord( $title, $this->wikiId );
                $this->initializeMutableRevisionFromArray( $revision, $fields );
-               $revision->setSlot( $mainSlot );
+
+               if ( isset( $fields['content'] ) && is_array( $fields['content'] ) ) {
+                       foreach ( $fields['content'] as $role => $content ) {
+                               $revision->setContent( $role, $content );
+                       }
+               } else {
+                       $mainSlot = $this->emulateMainSlot_1_29( $fields, $queryFlags, $title );
+                       $revision->setSlot( $mainSlot );
+               }
 
                return $revision;
        }
@@ -1575,7 +1947,7 @@ class RevisionStore
        private function fetchRevisionRowFromConds( IDatabase $db, $conditions, $flags = 0 ) {
                $this->checkDatabaseWikiId( $db );
 
-               $revQuery = self::getQueryInfo( [ 'page', 'user' ] );
+               $revQuery = $this->getQueryInfo( [ 'page', 'user' ] );
                $options = [];
                if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
                        $options[] = 'FOR UPDATE';
@@ -1590,18 +1962,58 @@ class RevisionStore
                );
        }
 
+       /**
+        * Finds the ID of a content row for a given revision and slot role.
+        * This can be used to re-use content rows even while the content ID
+        * is still missing from SlotRecords, in MIGRATION_WRITE_BOTH mode.
+        *
+        * @todo remove after MCR schema migration is complete.
+        *
+        * @param IDatabase $db
+        * @param int $revId
+        * @param string $role
+        *
+        * @return int|null
+        */
+       private function findSlotContentId( IDatabase $db, $revId, $role ) {
+               if ( $this->mcrMigrationStage < MIGRATION_WRITE_BOTH ) {
+                       return null;
+               }
+
+               try {
+                       $roleId = $this->slotRoleStore->getId( $role );
+                       $conditions = [
+                               'slot_revision_id' => $revId,
+                               'slot_role_id' => $roleId,
+                       ];
+
+                       $contentId = $db->selectField( 'slots', 'slot_content_id', $conditions, __METHOD__ );
+
+                       return $contentId ?: null;
+               } catch ( NameTableAccessException $ex ) {
+                       // If the role is missing from the slot_roles table,
+                       // the corresponding row in slots cannot exist.
+                       return null;
+               }
+       }
+
        /**
         * Return the tables, fields, and join conditions to be selected to create
-        * a new revision object.
+        * a new RevisionStoreRecord object.
         *
         * MCR migration note: this replaces Revision::getQueryInfo
         *
+        * If the format of fields returned changes in any way then the cache key provided by
+        * self::getRevisionRowCacheKey should be updated.
+        *
         * @since 1.31
         *
         * @param array $options Any combination of the following strings
         *  - 'page': Join with the page table, and select fields to identify the page
         *  - 'user': Join with the user table, and select the user name
-        *  - 'text': Join with the text table, and select fields to load page text
+        *  - 'text': Join with the text table, and select fields to load page text. This
+        *    option is deprecated in MW 1.32 with MCR migration stage MIGRATION_WRITE_BOTH,
+        *    and disallowed with MIGRATION_MEW.
         *
         * @return array With three keys:
         *  - tables: (string[]) to include in the `$table` to `IDatabase->select()`
@@ -1619,7 +2031,6 @@ class RevisionStore
                $ret['fields'] = array_merge( $ret['fields'], [
                        'rev_id',
                        'rev_page',
-                       'rev_text_id',
                        'rev_timestamp',
                        'rev_minor_edit',
                        'rev_deleted',
@@ -1638,9 +2049,13 @@ class RevisionStore
                $ret['fields'] = array_merge( $ret['fields'], $actorQuery['fields'] );
                $ret['joins'] = array_merge( $ret['joins'], $actorQuery['joins'] );
 
-               if ( $this->contentHandlerUseDB ) {
-                       $ret['fields'][] = 'rev_content_format';
-                       $ret['fields'][] = 'rev_content_model';
+               if ( $this->mcrMigrationStage < MIGRATION_NEW ) {
+                       $ret['fields'][] = 'rev_text_id';
+
+                       if ( $this->contentHandlerUseDB ) {
+                               $ret['fields'][] = 'rev_content_format';
+                               $ret['fields'][] = 'rev_content_model';
+                       }
                }
 
                if ( in_array( 'page', $options, true ) ) {
@@ -1666,6 +2081,12 @@ class RevisionStore
                }
 
                if ( in_array( 'text', $options, true ) ) {
+                       if ( $this->mcrMigrationStage === MIGRATION_NEW ) {
+                               throw new InvalidArgumentException( 'text table can no longer be joined directly' );
+                       } elseif ( $this->mcrMigrationStage >= MIGRATION_WRITE_BOTH ) {
+                               wfDeprecated( __METHOD__ . ' with `text` option', '1.32' );
+                       }
+
                        $ret['tables'][] = 'text';
                        $ret['fields'] = array_merge( $ret['fields'], [
                                'old_text',
@@ -1679,7 +2100,81 @@ class RevisionStore
 
        /**
         * Return the tables, fields, and join conditions to be selected to create
-        * a new archived revision object.
+        * a new SlotRecord.
+        *
+        * @since 1.32
+        *
+        * @param array $options Any combination of the following strings
+        *  - 'content': Join with the content table, and select content meta-data fields
+        *
+        * @return array With three keys:
+        *  - tables: (string[]) to include in the `$table` to `IDatabase->select()`
+        *  - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
+        *  - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
+        */
+       public function getSlotsQueryInfo( $options = [] ) {
+               $ret = [
+                       'tables' => [],
+                       'fields' => [],
+                       'joins'  => [],
+               ];
+
+               if ( $this->mcrMigrationStage < MIGRATION_NEW ) {
+                       $db = $this->getDBConnectionRef( DB_REPLICA );
+                       $ret['tables']['slots'] = 'revision';
+
+                       $ret['fields']['slot_revision_id'] = 'slots.rev_id';
+                       $ret['fields']['slot_content_id'] = 'NULL';
+                       $ret['fields']['slot_origin'] = 'slots.rev_id';
+                       $ret['fields']['role_name'] = $db->addQuotes( 'main' );
+
+                       if ( in_array( 'content', $options, true ) ) {
+                               $ret['fields']['content_size'] = 'slots.rev_len';
+                               $ret['fields']['content_sha1'] = 'slots.rev_sha1';
+                               $ret['fields']['content_address']
+                                       = $db->buildConcat( [ $db->addQuotes( 'tt:' ), 'slots.rev_text_id' ] );
+
+                               if ( $this->contentHandlerUseDB ) {
+                                       $ret['fields']['model_name'] = 'slots.rev_content_model';
+                               } else {
+                                       $ret['fields']['model_name'] = 'NULL';
+                               }
+                       }
+
+                       // XXX: in MIGRATION_WRITE_BOTH mode, emulate *and* select - using a UNION?
+                       // See Anomie's idea at <https://gerrit.wikimedia.org/r/c/416465/
+                       // 8..10/includes/Storage/RevisionStore.php#2113>
+               } else {
+                       $ret['tables'][] = 'slots';
+                       $ret['tables'][] = 'slot_roles';
+                       $ret['fields'] = array_merge( $ret['fields'], [
+                               'slot_revision_id',
+                               'slot_content_id',
+                               'slot_origin',
+                               'role_name'
+                       ] );
+                       $ret['joins']['slot_roles'] = [ 'INNER JOIN', [ 'slot_role_id = role_id' ] ];
+
+                       if ( in_array( 'content', $options, true ) ) {
+                               $ret['tables'][] = 'content';
+                               $ret['tables'][] = 'content_models';
+                               $ret['fields'] = array_merge( $ret['fields'], [
+                                       'content_size',
+                                       'content_sha1',
+                                       'content_address',
+                                       'model_name'
+                               ] );
+                               $ret['joins']['content'] = [ 'INNER JOIN', [ 'slot_content_id = content_id' ] ];
+                               $ret['joins']['content_models'] = [ 'INNER JOIN', [ 'content_model = model_id' ] ];
+                       }
+               }
+
+               return $ret;
+       }
+
+       /**
+        * Return the tables, fields, and join conditions to be selected to create
+        * a new RevisionArchiveRecord object.
         *
         * MCR migration note: this replaces Revision::getArchiveQueryInfo
         *
@@ -1701,7 +2196,6 @@ class RevisionStore
                                        'ar_namespace',
                                        'ar_title',
                                        'ar_rev_id',
-                                       'ar_text_id',
                                        'ar_timestamp',
                                        'ar_minor_edit',
                                        'ar_deleted',
@@ -1712,9 +2206,13 @@ class RevisionStore
                        'joins' => $commentQuery['joins'] + $actorQuery['joins'],
                ];
 
-               if ( $this->contentHandlerUseDB ) {
-                       $ret['fields'][] = 'ar_content_format';
-                       $ret['fields'][] = 'ar_content_model';
+               if ( $this->mcrMigrationStage < MIGRATION_NEW ) {
+                       $ret['fields'][] = 'ar_text_id';
+
+                       if ( $this->contentHandlerUseDB ) {
+                               $ret['fields'][] = 'ar_content_format';
+                               $ret['fields'][] = 'ar_content_model';
+                       }
                }
 
                return $ret;
@@ -1932,7 +2430,7 @@ class RevisionStore
                        return false;
                }
 
-               $revQuery = self::getQueryInfo();
+               $revQuery = $this->getQueryInfo();
                $res = $db->select(
                        $revQuery['tables'],
                        [
@@ -1990,7 +2488,7 @@ class RevisionStore
 
                $row = $this->cache->getWithSetCallback(
                        // Page/rev IDs passed in from DB to reflect history merges
-                       $this->cache->makeGlobalKey( 'revision-row-1.29', $db->getDomainID(), $pageId, $revId ),
+                       $this->getRevisionRowCacheKey( $db, $pageId, $revId ),
                        WANObjectCache::TTL_WEEK,
                        function ( $curValue, &$ttl, array &$setOpts ) use ( $db, $pageId, $revId ) {
                                $setOpts += Database::getCacheSetOptions( $db );
@@ -2014,6 +2512,26 @@ class RevisionStore
                }
        }
 
+       /**
+        * Get a cache key for use with a row as selected with getQueryInfo( [ 'page', 'user' ] )
+        * Caching rows without 'page' or 'user' could lead to issues.
+        * If the format of the rows returned by the query provided by getQueryInfo changes the
+        * cache key should be updated to avoid conflicts.
+        *
+        * @param IDatabase $db
+        * @param int $pageId
+        * @param int $revId
+        * @return string
+        */
+       private function getRevisionRowCacheKey( IDatabase $db, $pageId, $revId ) {
+               return $this->cache->makeGlobalKey(
+                       self::ROW_CACHE_KEY,
+                       $db->getDomainID(),
+                       $pageId,
+                       $revId
+               );
+       }
+
        // TODO: move relevant methods from Title here, e.g. getFirstRevision, isBigDeletion, etc.
 
 }
index 9462518..dff4b03 100644 (file)
@@ -38,7 +38,9 @@ use Wikimedia\Assert\Assert;
 class SlotRecord {
 
        /**
-        * @var object database result row, as a raw object
+        * @var object database result row, as a raw object. Callbacks are supported for field values,
+        *      to enable on-demand emulation of these values. This is primarily intended for use
+        *      during schema migration.
         */
        private $row;
 
@@ -142,11 +144,11 @@ class SlotRecord {
        /**
         * Constructs a complete SlotRecord for a newly saved revision, based on the incomplete
         * proto-slot. This adds information that has only become available during saving,
-        * particularly the revision ID and content address.
+        * particularly the revision ID, content ID and content address.
         *
         * @param int $revisionId the revision the slot is to be associated with (field slot_revision_id).
         *        If $protoSlot already has a revision, it must be the same.
-        * @param int $contentId the ID of the row in the content table describing the content
+        * @param int|null $contentId the ID of the row in the content table describing the content
         *        referenced by $contentAddress (field slot_content_id).
         *        If $protoSlot already has a content ID, it must be the same.
         * @param string $contentAddress the slot's content address (field content_address).
@@ -163,7 +165,8 @@ class SlotRecord {
                SlotRecord $protoSlot
        ) {
                Assert::parameterType( 'integer', $revisionId, '$revisionId' );
-               Assert::parameterType( 'integer', $contentId, '$contentId' );
+               // TODO once migration is over $contentId must be an integer
+               Assert::parameterType( 'integer|null', $contentId, '$contentId' );
                Assert::parameterType( 'string', $contentAddress, '$contentAddress' );
 
                if ( $protoSlot->hasRevision() && $protoSlot->getRevision() !== $revisionId ) {
@@ -181,7 +184,7 @@ class SlotRecord {
                        );
                }
 
-               if ( $protoSlot->hasAddress() && $protoSlot->getContentId() !== $contentId ) {
+               if ( $protoSlot->hasContentId() && $protoSlot->getContentId() !== $contentId ) {
                        throw new LogicException(
                                "Mismatching content ID $contentId: "
                                . "The slot already has content row {$protoSlot->getContentId()} associated."
@@ -231,11 +234,6 @@ class SlotRecord {
                Assert::parameterType( 'object', $row, '$row' );
                Assert::parameterType( 'Content|callable', $content, '$content' );
 
-               Assert::parameter(
-                       property_exists( $row, 'slot_id' ),
-                       '$row->slot_id',
-                       'must exist'
-               );
                Assert::parameter(
                        property_exists( $row, 'slot_revision_id' ),
                        '$row->slot_revision_id',
@@ -379,6 +377,13 @@ class SlotRecord {
         * @return bool whether this record contains the given field
         */
        private function hasField( $name ) {
+               if ( isset( $this->row->$name ) ) {
+                       // if the field is a callback, resolve first, then re-check
+                       if ( !is_string( $this->row->$name ) && is_callable( $this->row->$name ) ) {
+                               $this->getField( $name );
+                       }
+               }
+
                return isset( $this->row->$name );
        }
 
@@ -430,6 +435,30 @@ class SlotRecord {
                return $this->hasField( 'content_address' );
        }
 
+       /**
+        * Whether this slot has an origin (revision ID that originated the slot's content.
+        *
+        * @since 1.32
+        *
+        * @return bool
+        */
+       public function hasOrigin() {
+               return $this->hasField( 'slot_origin' );
+       }
+
+       /**
+        * Whether this slot has a content ID. Slots will have a content ID if their
+        * content has been stored in the content table. While building a new revision,
+        * SlotRecords will not have an ID associated.
+        *
+        * @since 1.32
+        *
+        * @return bool
+        */
+       public function hasContentId() {
+               return $this->hasField( 'slot_content_id' );
+       }
+
        /**
         * Whether this slot has revision ID associated. Slots will have a revision ID associated
         * only if they were loaded as part of an existing revision. While building a new revision,
index 91d8de4..f6e5912 100644 (file)
@@ -979,7 +979,7 @@ class Title implements LinkTarget {
                        && ( !$this->mContentModel || $flags === self::GAID_FOR_UPDATE )
                        && $this->getArticleID( $flags )
                ) {
-                       $linkCache = LinkCache::singleton();
+                       $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                        $linkCache->addLinkObj( $this ); # in case we already had an article ID
                        $this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' );
                }
@@ -3433,7 +3433,7 @@ class Title implements LinkTarget {
                        $this->mArticleID = 0;
                        return $this->mArticleID;
                }
-               $linkCache = LinkCache::singleton();
+               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                if ( $flags & self::GAID_FOR_UPDATE ) {
                        $oldUpdate = $linkCache->forUpdate( true );
                        $linkCache->clearLink( $this );
@@ -3463,7 +3463,7 @@ class Title implements LinkTarget {
                        return $this->mRedirect;
                }
 
-               $linkCache = LinkCache::singleton();
+               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                $linkCache->addLinkObj( $this ); # in case we already had an article ID
                $cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' );
                if ( $cached === null ) {
@@ -3497,7 +3497,7 @@ class Title implements LinkTarget {
                        $this->mLength = 0;
                        return $this->mLength;
                }
-               $linkCache = LinkCache::singleton();
+               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                $linkCache->addLinkObj( $this ); # in case we already had an article ID
                $cached = $linkCache->getGoodLinkFieldObj( $this, 'length' );
                if ( $cached === null ) {
@@ -3525,7 +3525,7 @@ class Title implements LinkTarget {
                        $this->mLatestID = 0;
                        return $this->mLatestID;
                }
-               $linkCache = LinkCache::singleton();
+               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                $linkCache->addLinkObj( $this ); # in case we already had an article ID
                $cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' );
                if ( $cached === null ) {
@@ -3550,7 +3550,7 @@ class Title implements LinkTarget {
         * @param int $newid The new Article ID
         */
        public function resetArticleID( $newid ) {
-               $linkCache = LinkCache::singleton();
+               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                $linkCache->clearLink( $this );
 
                if ( $newid === false ) {
@@ -3572,7 +3572,7 @@ class Title implements LinkTarget {
        }
 
        public static function clearCaches() {
-               $linkCache = LinkCache::singleton();
+               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                $linkCache->clear();
 
                $titleCache = self::getTitleCache();
@@ -3676,7 +3676,7 @@ class Title implements LinkTarget {
 
                $retVal = [];
                if ( $res->numRows() ) {
-                       $linkCache = LinkCache::singleton();
+                       $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                        foreach ( $res as $row ) {
                                $titleObj = self::makeTitle( $row->page_namespace, $row->page_title );
                                if ( $titleObj ) {
@@ -3744,7 +3744,7 @@ class Title implements LinkTarget {
                );
 
                $retVal = [];
-               $linkCache = LinkCache::singleton();
+               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                foreach ( $res as $row ) {
                        if ( $row->page_id ) {
                                $titleObj = self::newFromRow( $row );
@@ -4943,7 +4943,7 @@ class Title implements LinkTarget {
                // check, if the page language could be saved in the database, and if so and
                // the value is not requested already, lookup the page language using LinkCache
                if ( $wgPageLanguageUseDB && $this->mDbPageLanguage === false ) {
-                       $linkCache = LinkCache::singleton();
+                       $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                        $linkCache->addLinkObj( $this );
                        $this->mDbPageLanguage = $linkCache->getGoodLinkFieldObj( $this, 'lang' );
                }
@@ -4983,7 +4983,7 @@ class Title implements LinkTarget {
                        $langObj = $contentHandler->getPageLanguage( $this );
                        $this->mPageLanguage = [ $langObj->getCode(), $wgLanguageCode ];
                } else {
-                       $langObj = wfGetLangObj( $this->mPageLanguage[0] );
+                       $langObj = Language::factory( $this->mPageLanguage[0] );
                }
 
                return $langObj;
index 0208a72..0e5999d 100644 (file)
@@ -32,6 +32,22 @@ class WebResponse {
         */
        protected static $setCookies = [];
 
+       /** @var bool Used to disable setters before running jobs post-request (T191537) */
+       protected static $disableForPostSend = false;
+
+       /**
+        * Disable setters for post-send processing
+        *
+        * After this call, self::setCookie(), self::header(), and
+        * self::statusHeader() will log a warning and return without
+        * setting cookies or headers.
+        *
+        * @since 1.32
+        */
+       public static function disableForPostSend() {
+               self::$disableForPostSend = true;
+       }
+
        /**
         * Output an HTTP header, wrapper for PHP's header()
         * @param string $string Header to output
@@ -39,6 +55,16 @@ class WebResponse {
         * @param null|int $http_response_code Forces the HTTP response code to the specified value.
         */
        public function header( $string, $replace = true, $http_response_code = null ) {
+               if ( self::$disableForPostSend ) {
+                       wfDebugLog( 'header', 'ignored post-send header {header}', 'all', [
+                               'header' => $string,
+                               'replace' => $replace,
+                               'http_response_code' => $http_response_code,
+                               'exception' => new RuntimeException( 'Ignored post-send header' ),
+                       ] );
+                       return;
+               }
+
                \MediaWiki\HeaderCallback::warnIfHeadersSent();
                if ( $http_response_code ) {
                        header( $string, $replace, $http_response_code );
@@ -69,6 +95,14 @@ class WebResponse {
         * @param int $code Status code
         */
        public function statusHeader( $code ) {
+               if ( self::$disableForPostSend ) {
+                       wfDebugLog( 'header', 'ignored post-send status header {code}', 'all', [
+                               'code' => $code,
+                               'exception' => new RuntimeException( 'Ignored post-send status header' ),
+                       ] );
+                       return;
+               }
+
                HttpStatus::header( $code );
        }
 
@@ -117,20 +151,29 @@ class WebResponse {
                        $expire = time() + $wgCookieExpiration;
                }
 
+               $cookie = $options['prefix'] . $name;
+               $data = [
+                       'name' => (string)$cookie,
+                       'value' => (string)$value,
+                       'expire' => (int)$expire,
+                       'path' => (string)$options['path'],
+                       'domain' => (string)$options['domain'],
+                       'secure' => (bool)$options['secure'],
+                       'httpOnly' => (bool)$options['httpOnly'],
+               ];
+
+               if ( self::$disableForPostSend ) {
+                       wfDebugLog( 'cookie', 'ignored post-send cookie {cookie}', 'all', [
+                               'cookie' => $cookie,
+                               'data' => $data,
+                               'exception' => new RuntimeException( 'Ignored post-send cookie' ),
+                       ] );
+                       return;
+               }
+
                $func = $options['raw'] ? 'setrawcookie' : 'setcookie';
 
                if ( Hooks::run( 'WebResponseSetCookie', [ &$name, &$value, &$expire, &$options ] ) ) {
-                       $cookie = $options['prefix'] . $name;
-                       $data = [
-                               'name' => (string)$cookie,
-                               'value' => (string)$value,
-                               'expire' => (int)$expire,
-                               'path' => (string)$options['path'],
-                               'domain' => (string)$options['domain'],
-                               'secure' => (bool)$options['secure'],
-                               'httpOnly' => (bool)$options['httpOnly'],
-                       ];
-
                        // Per RFC 6265, key is name + domain + path
                        $key = "{$data['name']}\n{$data['domain']}\n{$data['path']}";
 
@@ -234,10 +277,7 @@ class FauxResponse extends WebResponse {
        public function getHeader( $key ) {
                $key = strtoupper( $key );
 
-               if ( isset( $this->headers[$key] ) ) {
-                       return $this->headers[$key];
-               }
-               return null;
+               return $this->headers[$key] ?? null;
        }
 
        /**
@@ -303,10 +343,7 @@ class FauxResponse extends WebResponse {
         * @return array|null
         */
        public function getCookieData( $name ) {
-               if ( isset( $this->cookies[$name] ) ) {
-                       return $this->cookies[$name];
-               }
-               return null;
+               return $this->cookies[$name] ?? null;
        }
 
        /**
index 89f2f41..5d7406c 100644 (file)
@@ -70,11 +70,7 @@ class XmlSelect {
         * @return string|null
         */
        public function getAttribute( $name ) {
-               if ( isset( $this->attributes[$name] ) ) {
-                       return $this->attributes[$name];
-               } else {
-                       return null;
-               }
+               return $this->attributes[$name] ?? null;
        }
 
        /**
index 48303a5..515ebc5 100644 (file)
@@ -771,7 +771,8 @@ class ApiPageSet extends ApiBase {
                // Store Title object in various data structures
                $title = Title::newFromRow( $row );
 
-               LinkCache::singleton()->addGoodLinkObjFromRow( $title, $row );
+               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
+               $linkCache->addGoodLinkObjFromRow( $title, $row );
 
                $pageId = intval( $row->page_id );
                $this->mAllPages[$row->page_namespace][$row->page_title] = $pageId;
@@ -904,7 +905,7 @@ class ApiPageSet extends ApiBase {
                        // Any items left in the $remaining list are added as missing
                        if ( $processTitles ) {
                                // The remaining titles in $remaining are non-existent pages
-                               $linkCache = LinkCache::singleton();
+                               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                                foreach ( $remaining as $ns => $dbkeys ) {
                                        foreach ( array_keys( $dbkeys ) as $dbkey ) {
                                                $title = Title::makeTitle( $ns, $dbkey );
index 096122d..3a60471 100644 (file)
@@ -697,7 +697,7 @@ class ApiParse extends ApiBase {
                        $hiddencats[$row->page_title] = isset( $row->pp_propname );
                }
 
-               $linkCache = LinkCache::singleton();
+               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
 
                foreach ( $links as $link => $sortkey ) {
                        $entry = [];
index 2fbc518..04d3f2c 100644 (file)
@@ -51,7 +51,12 @@ class ApiQueryPrefixSearch extends ApiQueryGeneratorBase {
                $offset = $params['offset'];
 
                $searchEngine = $this->buildSearchEngine( $params );
-               $titles = $searchEngine->extractTitles( $searchEngine->completionSearchWithVariants( $search ) );
+               $suggestions = $searchEngine->completionSearchWithVariants( $search );
+               $titles = $searchEngine->extractTitles( $suggestions );
+
+               if ( $suggestions->hasMoreResults() ) {
+                       $this->setContinueEnumParameter( 'offset', $offset + $limit );
+               }
 
                if ( $resultPageSet ) {
                        $resultPageSet->setRedirectMergePolicy( function ( array $current, array $new ) {
@@ -60,10 +65,6 @@ class ApiQueryPrefixSearch extends ApiQueryGeneratorBase {
                                }
                                return $current;
                        } );
-                       if ( count( $titles ) > $limit ) {
-                               $this->setContinueEnumParameter( 'offset', $offset + $limit );
-                               array_pop( $titles );
-                       }
                        $resultPageSet->populateFromTitles( $titles );
                        foreach ( $titles as $index => $title ) {
                                $resultPageSet->setGeneratorData( $title, [ 'index' => $index + $offset + 1 ] );
@@ -72,10 +73,6 @@ class ApiQueryPrefixSearch extends ApiQueryGeneratorBase {
                        $result = $this->getResult();
                        $count = 0;
                        foreach ( $titles as $title ) {
-                               if ( ++$count > $limit ) {
-                                       $this->setContinueEnumParameter( 'offset', $offset + $limit );
-                                       break;
-                               }
                                $vals = [
                                        'ns' => intval( $title->getNamespace() ),
                                        'title' => $title->getPrefixedText(),
@@ -86,8 +83,9 @@ class ApiQueryPrefixSearch extends ApiQueryGeneratorBase {
                                        $vals['pageid'] = intval( $title->getArticleID() );
                                }
                                $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $vals );
+                               ++$count;
                                if ( !$fit ) {
-                                       $this->setContinueEnumParameter( 'offset', $offset + $count - 1 );
+                                       $this->setContinueEnumParameter( 'offset', $offset + $count );
                                        break;
                                }
                        }
index 7d46a5f..3d87a5f 100644 (file)
@@ -60,6 +60,9 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
 
                // Create search engine instance and set options
                $search = $this->buildSearchEngine( $params );
+               if ( isset( $params['sort'] ) ) {
+                       $search->setSort( $params['sort'] );
+               }
                $search->setFeatureData( 'rewrite', (bool)$params['enablerewrites'] );
                $search->setFeatureData( 'interwiki', (bool)$interwiki );
 
@@ -142,20 +145,16 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
                $terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
                $titles = [];
                $count = 0;
-               $result = $matches->next();
                $limit = $params['limit'];
 
-               while ( $result ) {
-                       if ( ++$count > $limit ) {
-                               // We've reached the one extra which shows that there are
-                               // additional items to be had. Stop here...
-                               $this->setContinueEnumParameter( 'offset', $params['offset'] + $params['limit'] );
-                               break;
-                       }
+               if ( $matches->hasMoreResults() ) {
+                       $this->setContinueEnumParameter( 'offset', $params['offset'] + $params['limit'] );
+               }
 
+               foreach ( $matches as $result ) {
+                       $count++;
                        // Silently skip broken and missing titles
                        if ( $result->isBrokenTitle() || $result->isMissingRevision() ) {
-                               $result = $matches->next();
                                continue;
                        }
 
@@ -172,8 +171,6 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
                        } else {
                                $titles[] = $result->getTitle();
                        }
-
-                       $result = $matches->next();
                }
 
                // Here we assume interwiki results do not count with
@@ -301,8 +298,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
                                // Include number of results if requested
                                $totalhits += $interwikiMatches->getTotalHits();
 
-                               $result = $interwikiMatches->next();
-                               while ( $result ) {
+                               foreach ( $interwikiMatches as $result ) {
                                        $title = $result->getTitle();
                                        $vals = $this->getSearchResultData( $result, $prop, $terms );
 
@@ -322,8 +318,6 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
                                                // pagination info so just bail out
                                                break;
                                        }
-
-                                       $result = $interwikiMatches->next();
                                }
                        }
                        if ( $totalhits !== null ) {
@@ -391,6 +385,20 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
                        'enablerewrites' => false,
                ];
 
+               // If we have more than one engine the list of available sorts is
+               // difficult to represent. For now don't expose it.
+               $alternatives = MediaWiki\MediaWikiServices::getInstance()
+                       ->getSearchEngineConfig()
+                       ->getSearchTypes();
+               if ( count( $alternatives ) == 1 ) {
+                       $this->allowedParams['sort'] = [
+                               ApiBase::PARAM_DFLT => 'relevance',
+                               ApiBase::PARAM_TYPE => MediaWiki\MediaWikiServices::getInstance()
+                                       ->newSearchEngine()
+                                       ->getValidSorts(),
+                       ];
+               }
+
                return $this->allowedParams;
        }
 
index 40d4778..fc6eddf 100644 (file)
@@ -76,7 +76,7 @@ trait SearchApi {
                        if ( $alternatives[0] === null ) {
                                $alternatives[0] = self::$BACKEND_NULL_PARAM;
                        }
-                       $this->allowedParams['backend'] = [
+                       $params['backend'] = [
                                ApiBase::PARAM_DFLT => $searchConfig->getSearchType(),
                                ApiBase::PARAM_TYPE => $alternatives,
                        ];
@@ -140,8 +140,7 @@ trait SearchApi {
         * will be set:
         *  - backend: which search backend to use
         *  - limit: mandatory
-        *  - offset: optional, if set limit will be incremented by
-        *    one ( to support the continue parameter )
+        *  - offset: optional
         *  - namespace: mandatory
         *  - search engine profiles defined by SearchApi::getSearchProfileParams()
         * @param string[]|null $params API request params (must be sanitized by
@@ -157,15 +156,7 @@ trait SearchApi {
                        $searchEngine = MediaWikiServices::getInstance()->getSearchEngineFactory()->create( $type );
                        $limit = $params['limit'];
                        $searchEngine->setNamespaces( $params['namespace'] );
-                       $offset = null;
-                       if ( isset( $params['offset'] ) ) {
-                               // If the API supports offset then it probably
-                               // wants to fetch limit+1 so it can check if
-                               // more results are available to properly set
-                               // the continue param
-                               $offset = $params['offset'];
-                               $limit += 1;
-                       }
+                       $offset = isset( $params['offset'] ) ? $params['offset'] : null;
                        $searchEngine->setLimitOffset( $limit, $offset );
 
                        // Initialize requested search profiles.
index f19ee11..ca8ca7a 100644 (file)
        "apihelp-query+filearchive-paramvalue-prop-archivename": "يضيف اسم ملف إصدار الأرشيف للإصدارات غير الأحدث.",
        "apihelp-query+filearchive-example-simple": "عرض قائمة بجميع الملفات المحذوفة.",
        "apihelp-query+filerepoinfo-summary": "إرجاع معلومات التعريف حول مستودعات الصور المكونة في الويكي.",
+       "apihelp-query+filerepoinfo-param-prop": "أي خصائص المستودع يمكن الحصول عليها (قد يكون هناك المزيد متاح في بعض الويكيات): \n;apiurl:مسار إلى API المستودع ، مفيد للحصول على معلومات الصورة من المضيف.\n;name: مفتاح المستودع، يُستخدَم على سبيل المثال في قيم عودة <var>[[mw:Special:MyLanguage/Manual:$wgForeignFileRepos|$wgForeignFileRepos]]</var> و[[Special:ApiHelp/query+imageinfo|معلومات الصورة]].",
+       "apihelp-query+filerepoinfo-example-simple": "الحصول على معلومات حول مستودعات الملفات.",
+       "apihelp-query+fileusage-summary": "ابحث عن كل الصفحات التي تستخدم الملفات المعطاة.",
        "apihelp-query+fileusage-param-prop": "أي الخصائص للحصول عليها.",
+       "apihelp-query+fileusage-paramvalue-prop-pageid": "معرف الصفحة لكل صفحة.",
+       "apihelp-query+fileusage-paramvalue-prop-title": "عنوان كل صفحة.",
+       "apihelp-query+fileusage-paramvalue-prop-redirect": "علم إذا كانت الصفحة تحويلة.",
+       "apihelp-query+fileusage-param-namespace": "إدراج الصفحات في هذه النطاقات فقط.",
+       "apihelp-query+fileusage-param-limit": "كم العدد للعودة.",
+       "apihelp-query+fileusage-param-show": "إظهار العناصر التي تستوفي هذه المعايير فقط:\n;تحويلة: عرض التحويلات فقط.\n;غير تحويلة:إظهار غير التحويلات فقط.",
+       "apihelp-query+fileusage-example-simple": "احصل على قائمة الصفحات التي تستخدم [[:File:Example.jpg]].",
+       "apihelp-query+fileusage-example-generator": "احصل على معلومات حول الصفحات التي تستخدم [[:File:Example.jpg]].",
+       "apihelp-query+imageinfo-summary": "يعرض معلومات الملف وسجل الرفع.",
+       "apihelp-query+imageinfo-param-prop": "أي المعلومات عن الصورة للحصول عليها",
        "apihelp-query+imageinfo-paramvalue-prop-timestamp": "يضيف الطابع الزمني للنسخة المرفوعة.",
+       "apihelp-query+imageinfo-paramvalue-prop-user": "يضيف المستخدم الذي رفع كل إصدار من الملف.",
        "apihelp-query+imageinfo-paramvalue-prop-userid": "إضافة هوية المستخدم الذي قام بتحميل كل إصدار ملف.",
+       "apihelp-query+imageinfo-paramvalue-prop-comment": "تعليق على الإصدار.",
+       "apihelp-query+imageinfo-paramvalue-prop-parsedcomment": "تحليل التعليق على النسخة.",
+       "apihelp-query+imageinfo-paramvalue-prop-canonicaltitle": "يضيف العنوان الأساسي للملف.",
+       "apihelp-query+imageinfo-paramvalue-prop-url": "يعطي مسارا للملف وصفحة الوصف.",
+       "apihelp-query+imageinfo-paramvalue-prop-size": "يضيف حجم الملف بالبايت والطول والعرض وعدد الصفحات (عند قابلية التطبيق).",
        "apihelp-query+imageinfo-paramvalue-prop-dimensions": "الاسم المستعار للحجم.",
+       "apihelp-query+imageinfo-paramvalue-prop-sha1": "يضيف تجزئة SHA-1 للملف.",
+       "apihelp-query+imageinfo-paramvalue-prop-mime": "يضيف نوع MIME للملف.",
+       "apihelp-query+imageinfo-paramvalue-prop-thumbmime": "يضيف نوع MIME للصورة المصغرة (يتطلب مسار ووسيط $1urlwidth).",
+       "apihelp-query+imageinfo-paramvalue-prop-mediatype": "يضيف نوع الوسائط للملف.",
+       "apihelp-query+imageinfo-paramvalue-prop-metadata": "يسرد بيانات تعريف Exif لإصدار الملف.",
+       "apihelp-query+imageinfo-paramvalue-prop-commonmetadata": "يسرد بيانات التعريف العامة لصيغة الملف لإصدار الملف.",
+       "apihelp-query+imageinfo-paramvalue-prop-extmetadata": "يسرد البيانات الوصفية المنسقة من مصادر متعددة، النتائج بتنسيق HTML.",
+       "apihelp-query+imageinfo-paramvalue-prop-archivename": "يضيف اسم ملف إصدار الأرشيف للإصدارات غير الأحدث.",
+       "apihelp-query+imageinfo-paramvalue-prop-bitdepth": "يضيف عمق البت للإصدار.",
+       "apihelp-query+imageinfo-paramvalue-prop-uploadwarning": "تُستخدَم من قبل صفحة Special:Upload للحصول على معلومات حول ملف موجود، غير مخصص للاستخدام خارج نواة ميدياويكي.",
+       "apihelp-query+imageinfo-paramvalue-prop-badfile": "يضيف ما إذا كان الملف موجودا في [[MediaWiki:Bad image list]]",
+       "apihelp-query+imageinfo-param-limit": "عدد مراجعات الملفات ليتم إرجاعها لكل ملف.",
+       "apihelp-query+imageinfo-param-start": "الطابع الزمني لبدء الإدراج منه.",
+       "apihelp-query+imageinfo-param-end": "الطابع الزمني لإيقاف الإدراج فيه.",
+       "apihelp-query+imageinfo-param-urlwidth": "إذا تم تعيين $2prop=url، فسيتم إرجاع مسار صورة مقسمة إلى هذا العرض،\nلأسباب تتعلق بالأداء في حالة استخدام هذا الخيار; لن يتم عرض أكثر من $1 من الصور المقاسة.",
+       "apihelp-query+imageinfo-param-urlheight": "على غرار $1urlwidth.",
+       "apihelp-query+imageinfo-param-metadataversion": "إصدار البيانات الوصفية لاستخدامه، إذا تم تحديد <kbd>latest</kbd>، استخدم أحدث إصدار، الافتراضي <kbd>1</kbd> للتوافق مع الإصدارات السابقة.",
+       "apihelp-query+imageinfo-param-extmetadatalanguage": "أية لغة لجلب extmetadata بها، هذا يؤثر على كل الترجمة التي يتم جلبها، إذا كانت متعددة متاحة، وكذلك كيفية تنسيق أشياء مثل الأرقام والقيم المختلفة.",
+       "apihelp-query+imageinfo-param-extmetadatamultilang": "إذا كانت ترجمات خاصية extmetadata متاحة، فاجلبها كلها.",
+       "apihelp-query+imageinfo-param-extmetadatafilter": "إذا كان محددا وغير فارغ، فسيتم إرجاع هذه المفاتيح فقط لـ$1prop=extmetadata.",
+       "apihelp-query+imageinfo-param-urlparam": "سلسلة وسيط خاصة بالمعالج، على سبيل المثال، قد تستخدم ملفات PDF <kbd>page15-100px</kbd>، يجب استخدام <var>$1urlwidth</var> وأن يكون متسقا مع <var>$1urlparam</var>.",
+       "apihelp-query+imageinfo-param-badfilecontexttitle": "إذا تم تعيين<kbd>$2prop=badfile</kbd> ، فهذا هو عنوان الصفحة المستخدم عند تقييم [[MediaWiki:Bad image list]]",
+       "apihelp-query+imageinfo-param-localonly": "ابحث فقط عن الملفات في المستودع المحلي.",
+       "apihelp-query+imageinfo-example-simple": "إحضار معلومات حول الإصدار الحالي من [[:File:Albert Einstein Head.jpg]].",
+       "apihelp-query+imageinfo-example-dated": "إحضار معلومات حول إصدارات [[:File:Test.jpg]] من عام 2008 وما بعده.",
+       "apihelp-query+images-summary": "يعرض جميع الملفات المضمنة في الصفحات المعينة.",
+       "apihelp-query+images-param-limit": "كم عدد الملفات للعودة.",
+       "apihelp-query+images-param-images": "إدراج هذه الملفات فقط، مفيد للتحقق ما إذا كانت صفحة معينة تحتوي على ملف معين.",
        "apihelp-query+images-param-dir": "الاتجاه للإدراج فيه.",
+       "apihelp-query+images-example-simple": "احصل على قائمة بالملفات المستخدمة في [[Main Page]].",
+       "apihelp-query+images-example-generator": "الحصول على معلومات حول جميع الملفات المستخدمة في [[Main Page]].",
+       "apihelp-query+imageusage-summary": "ابحث عن كل الصفحات التي تستخدم عنوان الصورة المعين.",
        "apihelp-query+imageusage-param-title": "عنوان للبحث، لا يمكن استخدامه مع <var>$1pageid</var",
        "apihelp-query+imageusage-param-pageid": "معرف صفحة للبحث، لا يمكن استخدامه مع <var>$1title</var>.",
        "apihelp-query+imageusage-param-namespace": "النطاق للتعداد.",
        "apihelp-query+imageusage-param-dir": "الاتجاه للإدراج فيه.",
+       "apihelp-query+imageusage-param-filterredir": "كيفية التصفية للتحويلات، إذا تم ضبطه على <kbd>nonredirects</kbd> عند تمكين <var>$1redirect</var>، فسيتم تطبيق هذا على المستوى الثاني فقط.",
+       "apihelp-query+imageusage-param-limit": "كم عدد الصفحات التي سيتم إرجاعها، إذا تم تمكين <var>$1redirect</var> ، فسيتم تطبيق الحد الأقصى على كل مستوى بشكل منفصل (مما يعني أنه قد يتم إرجاع ما يصل إلى * <var>$1limit</var> نتائج).",
+       "apihelp-query+imageusage-param-redirect": "إذا كان ربط الصفحة تحويلة، فابحث عن جميع الصفحات التي تصل لتلك التحويلة أيضا، الحد الأقصى هو النصف.",
+       "apihelp-query+imageusage-example-simple": "عرض الصفحات التي تستخدم [[:File:Albert Einstein Head.jpg]].",
+       "apihelp-query+imageusage-example-generator": "احصل على معلومات حول الصفحات التي تستخدم [[:File:Albert Einstein Head.jpg]].",
+       "apihelp-query+info-summary": "الحصول على معلومات الصفحة الأساسية.",
+       "apihelp-query+info-param-prop": "أي الخصائص الإضافية للحصول عليها:",
+       "apihelp-query+info-paramvalue-prop-protection": "إدراج مستوى الحماية لكل صفحة.",
+       "apihelp-query+info-paramvalue-prop-talkid": "معرف الصفحة لصفحة النقاش لكل صفحة غير النقاش.",
+       "apihelp-query+info-paramvalue-prop-watched": "أدرج حالة المراقبة لكل صفحة.",
+       "apihelp-query+info-paramvalue-prop-watchers": "عدد المراقبين، إذا كان مسموحا.",
+       "apihelp-query+info-paramvalue-prop-visitingwatchers": "عدد مراقبي كل صفحة الذين زاروا التحريرات الأخيرة لتلك الصفحة، إذا كان مسموحا بذلك.",
+       "apihelp-query+info-paramvalue-prop-notificationtimestamp": "الطابع الزمني لإشعار قائمة المراقبة لكل صفحة.",
+       "apihelp-query+info-paramvalue-prop-subjectid": "معرف الصفحة للصفحة الرئيسية لكل صفحة نقاش.",
+       "apihelp-query+info-paramvalue-prop-url": "يعطي مسارا كاملا، ومسارا للتعديل، ومسار الأساسي لكل صفحة.",
+       "apihelp-query+info-paramvalue-prop-readable": "ما إذا كان يمكن للمستخدم قراءة هذه الصفحة.",
+       "apihelp-query+info-paramvalue-prop-preload": "يعطي النص الذي تم إرجاعه بواسطة EditFormPreloadText.",
+       "apihelp-query+info-paramvalue-prop-displaytitle": "يعطي الطريقة التي يتم بها عرض عنوان الصفحة بالفعل.",
+       "apihelp-query+info-paramvalue-prop-varianttitles": "يعطي عنوان العرض بجميع الصيغ الخاصة بلغة محتوى الموقع.",
+       "apihelp-query+info-param-testactions": "اختبر ما إذا كان المستخدم الحالي يمكنه تنفيذ إجراءات معينة على الصفحة.",
+       "apihelp-query+info-param-token": "استخدم [[Special:ApiHelp/query+tokens|action=query&meta=tokens]] بدلا من ذلك.",
+       "apihelp-query+info-example-simple": "الحصول على معلومات حول الصفحة <kbd>Main Page</kbd>.",
+       "apihelp-query+info-example-protection": "احصل على معلومات عامة وحماية حول الصفحة <kbd>Main Page</kbd>.",
+       "apihelp-query+iwbacklinks-summary": "العثور على جميع الصفحات التي تصل إلى وصلة الإنترويكي المعطاه.",
+       "apihelp-query+iwbacklinks-extended-description": "يمكن استخدامها للعثور على جميع الروابط ببادئة، أو كل الروابط إلى عنوان (ببادئة معينة)، استخدام أي من الوسيطين \"جميع روابط الإنترويكي\" على نحو فعال.",
+       "apihelp-query+iwbacklinks-param-prefix": "بادئة للإنترويكي.",
+       "apihelp-query+iwbacklinks-param-title": "رابط إنترويكي للبحث عنه، يجب استخدامه مع <var>$1blprefix</var.",
        "apihelp-query+iwbacklinks-param-limit": "كم عدد مجموع الصفحات للعودة.",
        "apihelp-query+iwbacklinks-param-prop": "أي الخصائص للحصول عليها.",
+       "apihelp-query+iwbacklinks-paramvalue-prop-iwprefix": "يضيف بادئة الإنترويكي.",
+       "apihelp-query+iwbacklinks-paramvalue-prop-iwtitle": "يضيف عنوان الإنترويكي.",
        "apihelp-query+iwbacklinks-param-dir": "الاتجاه للإدراج فيه.",
+       "apihelp-query+iwlinks-summary": "يعرض جميع روابط الإنترويكي من الصفحات المحددة.",
+       "apihelp-query+iwlinks-param-url": "ما إذا كنت تريد الحصول على المسار الكامل (لا يمكن استخدامه مع $1prop).",
+       "apihelp-query+iwlinks-param-prop": "الخصائص الإضافية التي يمكنك الحصول عليها لكل رابط بين اللغات:",
+       "apihelp-query+iwlinks-paramvalue-prop-url": "يضيف المسار الكامل.",
        "apihelp-query+iwlinks-param-limit": "كم عدد وصلات الإنترويكي للعودة.",
+       "apihelp-query+iwlinks-param-prefix": "عودة روابط الإنترويكي بهذه البادئة فقط.",
+       "apihelp-query+iwlinks-param-title": "رابط إنترويكي للبحث عنه، يجب استخدامه مع <var>$1prefix</var.",
        "apihelp-query+iwlinks-param-dir": "الاتجاه للإدراج فيه.",
+       "apihelp-query+iwlinks-example-simple": "الحصول على روابط إنترويكي من الصفحة <kbd>Main Page</kbd>.",
+       "apihelp-query+langbacklinks-summary": "ابحث عن جميع الصفحات التي تصل لرابط اللغة المحدد.",
+       "apihelp-query+langbacklinks-extended-description": "يمكن استخدامها للعثور على جميع الروابط برمز لغة، أو كل الروابط إلى عنوان (بلغة معينة)، استخدام أي من الوسيطين \"كل روابط اللغات\" بشكل فعال،\nلاحظ أن هذا قد لا يفكر في روابط اللغة التي تتم إضافتها بواسطة الإضافات.",
+       "apihelp-query+langbacklinks-param-lang": "لغة لرابط اللغة.",
+       "apihelp-query+langbacklinks-param-title": "رابط اللغة للبحث عنه، يجب استخدامه مع $1lang.",
        "apihelp-query+langbacklinks-param-limit": "كم عدد مجموع الصفحات للعودة.",
        "apihelp-query+langbacklinks-param-prop": "أي الخصائص للحصول عليها.",
+       "apihelp-query+langbacklinks-paramvalue-prop-lllang": "يضيف رمز لغة رابط اللغة.",
+       "apihelp-query+langbacklinks-paramvalue-prop-lltitle": "يضيف عنوان رابط اللغة.",
        "apihelp-query+langbacklinks-param-dir": "الاتجاه للإدراج فيه.",
+       "apihelp-query+langbacklinks-example-simple": "الحصول على الصفحات التي تصل إلى [[:fr:Test]].",
+       "apihelp-query+langbacklinks-example-generator": "الحصول على معلومات حول الصفحات التي تصل إلى [[:fr:Test]].",
+       "apihelp-query+langlinks-summary": "يعرض جميع روابط الإنترويكي التي تربط من الصفحات المحددة.",
+       "apihelp-query+langlinks-param-limit": "كم عدد الروابط المحلية التي ستعود.",
+       "apihelp-query+langlinks-param-url": "ما إذا كنت ستحصل على المسار الكامل (لا يمكن استخدامه مع <var>$1prop</var>).",
+       "apihelp-query+langlinks-param-prop": "الخصائص الإضافية التي يمكنك الحصول عليها لكل رابط بين اللغات:",
+       "apihelp-query+langlinks-paramvalue-prop-url": "يضيف المسار الكامل.",
+       "apihelp-query+langlinks-paramvalue-prop-langname": "يضيف اسم اللغة المترجمة (أفضل جهد)، استخدم <var>$1inlanguagecode</var> للتحكم في اللغة.",
+       "apihelp-query+langlinks-paramvalue-prop-autonym": "يضيف اسم اللغة الأم.",
+       "apihelp-query+langlinks-param-lang": "إرجاع روابط اللغة برمز اللغة هذا فقط.",
+       "apihelp-query+langlinks-param-title": "رابط للبحث عنه، يجب استخدامه مع <var>$1prefix</var.",
        "apihelp-query+langlinks-param-dir": "الاتجاه للإدراج فيه.",
+       "apihelp-query+langlinks-param-inlanguagecode": "رمز اللغة لأسماء اللغة المترجمة.",
+       "apihelp-query+langlinks-example-simple": "الحصول على روابط بين اللغات تربط من الصفحة <kbd>Main Page</kbd>.",
+       "apihelp-query+links-summary": "يعرض جميع الروابط من الصفحات المحددة.",
+       "apihelp-query+links-param-namespace": "إظهار الروابط في هذه النطاقات فقط.",
        "apihelp-query+links-param-limit": "كم عدد الوصلات للعودة.",
+       "apihelp-query+links-param-titles": "إدراج الروابط لهذه العناوين فقط، مفيد للتحقق مما إذا كانت صفحة معينة ترتبط بعنوان معين.",
        "apihelp-query+links-param-dir": "الاتجاه للإدراج فيه.",
+       "apihelp-query+links-example-simple": "الحصول على روابط من الصفحة <kbd>Main Page</kbd>.",
+       "apihelp-query+links-example-generator": "الحصول على معلومات حول صفحات الارتباط في الصفحة <kbd>Main Page</kbd>.",
+       "apihelp-query+links-example-namespaces": "احصل على روابط من الصفحة <kbd>Main Page</kbd> في النطاقات {{ns:user}} و{{ns:template}}.",
        "apihelp-query+linkshere-summary": "ابحث عن جميع الصفحات الموصولة للصفحة المحددة.",
        "apihelp-query+linkshere-param-prop": "أي الخصائص للحصول عليها.",
+       "apihelp-query+linkshere-paramvalue-prop-pageid": "معرف الصفحة لكل صفحة.",
+       "apihelp-query+linkshere-paramvalue-prop-title": "عنوان كل صفحة.",
+       "apihelp-query+linkshere-paramvalue-prop-redirect": "علم إذا كانت الصفحة تحويلة.",
+       "apihelp-query+linkshere-param-namespace": "إدراج الصفحات في هذه النطاقات فقط.",
+       "apihelp-query+linkshere-param-limit": "كم العدد للعودة.",
+       "apihelp-query+linkshere-param-show": "إظهار العناصر التي تستوفي هذه المعايير فقط:\n;تحويلة: عرض التحويلات فقط.\n;غير تحويلة:إظهار غير التحويلات فقط.",
+       "apihelp-query+linkshere-example-simple": "احصل على قائمة الصفحات التي تربط بـ[[Main Page]].",
+       "apihelp-query+linkshere-example-generator": "احصل على معلومات حول الصفحات التي تصل إلى [[Main Page]].",
+       "apihelp-query+logevents-summary": "الحصول على الأحداث من السجلات.",
        "apihelp-query+logevents-param-prop": "أي الخصائص للحصول عليها.",
+       "apihelp-query+logevents-paramvalue-prop-ids": "يضيف معرف حدث السجل.",
+       "apihelp-query+logevents-paramvalue-prop-title": "يضيف عنوان الصفحة لحدث السجل.",
+       "apihelp-query+logevents-paramvalue-prop-type": "يضيف نوع حدث السجل.",
+       "apihelp-query+logevents-paramvalue-prop-user": "يضيف المستخدم المسؤول عن حدث السجل.",
+       "apihelp-query+logevents-paramvalue-prop-userid": "يضيف معرف المستخدم الذي كان مسؤولا عن حدث السجل.",
+       "apihelp-query+logevents-paramvalue-prop-timestamp": "يضيف الطابع الزمني لحدث السجل.",
+       "apihelp-query+logevents-paramvalue-prop-comment": "يضيف تعليق حدث السجل.",
+       "apihelp-query+logevents-paramvalue-prop-parsedcomment": "يضيف التعليق المحلل لحدث السجل.",
+       "apihelp-query+logevents-paramvalue-prop-details": "يسرد تفاصيل إضافية حول حدث السجل.",
+       "apihelp-query+logevents-paramvalue-prop-tags": "يسرد وسوما لحدث السجل.",
+       "apihelp-query+logevents-param-type": "تصفية إدخالات السجل لهذا النوع فقط.",
+       "apihelp-query+logevents-param-action": "تصفية إجراءات السجل لهذا الإجراء فقط، يتجاوز <var>$1type</var>، في قائمة القيم المحتملة، يمكن أن تحتوي القيم بحرف بدل العلامة النجمية مثل <kbd>action/*</kbd> على سلاسل مختلفة بعد الخط المائل (/).",
        "apihelp-query+logevents-param-start": "الطابع الزمني لبدء العد منه.",
+       "apihelp-query+logevents-param-end": "الطابع الزمني لإنهاء التعداد.",
+       "apihelp-query+logevents-param-user": "تصفية الإلدخالات تلك المدخلات من قبل المستخدم المعطى.",
+       "apihelp-query+logevents-param-title": "تصفية الإدخالات إلى تلك المتعلقة بصفحة.",
+       "apihelp-query+logevents-param-namespace": "تصفية الإدخالات إلى تلك الموجودة في النطاق المحدد.",
+       "apihelp-query+logevents-param-prefix": "تصفية الإدخالات التي تبدأ بهذه البادئة.",
+       "apihelp-query+logevents-param-tag": "إدراج إدخالات الحدث الموسومة بهذ الوسم فقط.",
+       "apihelp-query+logevents-param-limit": "كم العدد الكلي لإدخالات الحدث للعودة.",
+       "apihelp-query+logevents-example-simple": "إدراج أحداث السجل الأخيرة.",
+       "apihelp-query+pagepropnames-summary": "إدراج جميع أسماء خواص الصفحة قيد الاستخدام في الويكي.",
+       "apihelp-query+pagepropnames-param-limit": "الحد الأقصى لعدد الأسماء للعودة.",
+       "apihelp-query+pagepropnames-example-simple": "الحصول على أول 10 أسماء خواص.",
+       "apihelp-query+pageprops-summary": "الحصول على خصائص صفحة مختلفة محددة في محتوى الصفحة.",
+       "apihelp-query+pageprops-param-prop": "سرد خصائص الصفحة هذه فقط (<kbd>[[Special:ApiHelp/query+pagepropnames|action=query&list=pagepropnames]]</kbd> تقوم بإرجاع أسماء خصائص الصفحة قيد الاستخدام)، مفيد للتحقق مما إذا كانت الصفحات تستخدم خاصية صفحة معينة.",
+       "apihelp-query+pageprops-example-simple": "احصل على خصائص للصفحات <kbd>Main Page</kbd> و<kbd>MediaWiki</kbd>.",
+       "apihelp-query+pageswithprop-summary": "سرد جميع الصفحات التي تستخدم خاصية صفحة معينة.",
+       "apihelp-query+pageswithprop-param-propname": "خاصية الصفحة التي تريد تعداد الصفحات (<kbd>[[Special:ApiHelp/query+pagepropnames|action=query&list=pagepropnames]]</kbd> تقوم بإرجاع أسماء خصائص الصفحة قيد الاستخدام).",
        "apihelp-query+pageswithprop-param-prop": "أية قطعة من المعلومات لتضمينها:",
        "apihelp-query+pageswithprop-paramvalue-prop-ids": "يضيف معرف الصفحة.",
        "apihelp-query+pageswithprop-paramvalue-prop-title": "يضيف معرف عنوان ونطاق الصفحة.",
+       "apihelp-query+pageswithprop-paramvalue-prop-value": "يضيف قيمة خاصية الصفحة.",
        "apihelp-query+pageswithprop-param-limit": "الحد الأقصى لعدد الصفحات المطلوب عرضها.",
        "apihelp-query+pageswithprop-param-dir": "في أي اتجاه للفرز.",
+       "apihelp-query+pageswithprop-example-simple": "أدرج أول 10 صفحات تستخدم <code>&#123;&#123;DISPLAYTITLE:&#125;&#125;</code>.",
+       "apihelp-query+pageswithprop-example-generator": "احصل على معلومات إضافية حول أول 10 صفحات تستخدم <code>_&#95;NOTOC_&#95;</code>.",
+       "apihelp-query+prefixsearch-summary": "قم بإجراء بحث بالبادئة عن عناوين الصفحات.",
+       "apihelp-query+prefixsearch-extended-description": "على الرغم من التشابه في الأسماء، لا يُقصَد بهذه الوحدة أن تكون مكافئة لـ [[Special:PrefixIndex]]، لذلك; راجع <kbd>[[Special:ApiHelp/query+allpages|action=query&list=allpages]]</kbd> مع الوسيط <kbd>apprefix</kbd>، يشبه الغرض من هذه الوحدة <kbd>[[Special:ApiHelp/opensearch|action=opensearch]]</kbd>: لأخذ مدخلات المستخدم وتقديم أفضل العناوين المطابقة، استنادا إلى الواجهة الخلفية لمحرك البحث، قد يتضمن هذا تصحيحا مطبعيا أو تجنبا للتحويل أو استدلالا آخر.",
+       "apihelp-query+prefixsearch-param-search": "سلسلة البحث.",
+       "apihelp-query+prefixsearch-param-namespace": "النطاقات للبحث، يتم التجاهل إذا بدأ <var>$1search</var> ببادئة نطاق صالحة.",
+       "apihelp-query+prefixsearch-param-limit": "الحد الأقصى لعدد النتائج للعودة.",
        "apihelp-query+prefixsearch-param-offset": "عدد النتائج المراد تخطيها.",
+       "apihelp-query+prefixsearch-example-simple": "ابحث عن عناوين الصفحات التي تبدأ بـ<kbd>meaning</kbd>.",
+       "apihelp-query+prefixsearch-param-profile": "ابحث عن ملف شخصي لاستخدامه.",
+       "apihelp-query+protectedtitles-summary": "سرد جميع العناوين المحمية من الإنشاء.",
+       "apihelp-query+protectedtitles-param-namespace": "إدراج عناوين في هذه النطاقات فقط.",
+       "apihelp-query+protectedtitles-param-level": "إدراج العناوين بمستويات الحماية هذه فقط.",
        "apihelp-query+protectedtitles-param-limit": "كم عدد مجموع الصفحات للعودة.",
+       "apihelp-query+protectedtitles-param-start": "بدء الإدراج في هذا الطابع الزمني للحماية.",
+       "apihelp-query+protectedtitles-param-end": "وقف الإدراج في هذا الطابع الزمني للحماية.",
        "apihelp-query+protectedtitles-param-prop": "أي الخصائص للحصول عليها.",
+       "apihelp-query+protectedtitles-paramvalue-prop-timestamp": "يإضيف الطابع الزمني عند إضافة الحماية.",
+       "apihelp-query+protectedtitles-paramvalue-prop-user": "يضيف المستخدم الذي أضاف الحماية.",
+       "apihelp-query+protectedtitles-paramvalue-prop-userid": "يضيف معرف المستخدم الذي أضاف الحماية.",
+       "apihelp-query+protectedtitles-paramvalue-prop-comment": "يضيف التعليق للحماية.",
+       "apihelp-query+protectedtitles-paramvalue-prop-parsedcomment": "يضيف تعليق التعليق المحلل للحماية.",
+       "apihelp-query+protectedtitles-paramvalue-prop-expiry": "يإضيف الطابع الزمني للوقت الذي سيتم فيه رفع الحماية.",
+       "apihelp-query+protectedtitles-paramvalue-prop-level": "يضيف مستوى الحماية.",
+       "apihelp-query+protectedtitles-example-simple": "سرد العناوين المحمية.",
+       "apihelp-query+protectedtitles-example-generator": "ابحث عن روابط للعناوين المحمية في النطاق الرئيسي.",
+       "apihelp-query+querypage-summary": "الحصول على قائمة يتم توفيرها من خلال صفحة خاصة تستند إلى صفحة استعلام.",
+       "apihelp-query+querypage-param-page": "اسم الصفحة الخاصة، ملاحظة: هذا حساس لحالة الأحرف.",
+       "apihelp-query+querypage-param-limit": "عدد النتائج للعودة.",
+       "apihelp-query+querypage-example-ancientpages": "إرجاع النتائج من [[Special:Ancientpages]].",
+       "apihelp-query+random-summary": "الحصول على مجموعة من الصفحات العشوائية.",
+       "apihelp-query+random-extended-description": "يتم سرد الصفحات بتسلسل ثابت، نقطة البداية فقط عشوائية: هذا يعني أنه إذا، على سبيل المثال، كانت <samp>Main Page</samp>  أول صفحة عشوائية في القائمة، <samp>List of fictional monkeys</samp> سوف تكون الثانية <em>always</em>، <samp>List of people on stamps of Vanuatu</samp> الثالثة، إلخ.",
+       "apihelp-query+random-param-namespace": "إرجاع الصفحات في هذه النطاقات فقط.",
+       "apihelp-query+random-param-limit": "تحديد عدد الصفحات العشوائية التي سيتم إرجاعها.",
+       "apihelp-query+random-param-redirect": "استخدم <kbd>$1filterredir=redirects</kbd> بدلا من ذلك.",
        "apihelp-query+random-param-filterredir": "كيفية التصفية للتحويلات.",
+       "apihelp-query+random-example-simple": "قم بإرجاع صفحتين عشوائيتين من النطاق الرئيسي.",
+       "apihelp-query+random-example-generator": "إرجاع معلومات الصفحة حول صفحتين عشوائيتين من النطاق الرئيسي.",
+       "apihelp-query+recentchanges-summary": "تعداد أحدث التغييرات.",
        "apihelp-query+recentchanges-param-start": "الطابع الزمني لبدء العد منه.",
+       "apihelp-query+recentchanges-param-end": "الطابع الزمني لإنهاء التعداد.",
+       "apihelp-query+recentchanges-param-namespace": "تصفية التغييرات على هذه النطاقات فقط.",
+       "apihelp-query+recentchanges-param-user": "إددراج التغييرات بواسطة هذا المستخدم فقط.",
+       "apihelp-query+recentchanges-param-excludeuser": "لا تسرد التغييرات بواسطة هذا المستخدم.",
+       "apihelp-query+recentchanges-param-tag": "إدراج التغييرات الموسومة بهذ الوسم فقط.",
+       "apihelp-query+recentchanges-param-prop": "تضمين أجزاء إضافية من المعلومات:",
+       "apihelp-query+recentchanges-paramvalue-prop-user": "يضيف المستخدم المسؤول عن التحرير والوسوم إذا كان يوجد آيبي.",
+       "apihelp-query+recentchanges-paramvalue-prop-userid": "يضيف المستخدم المسؤول عن التعديل.",
+       "apihelp-query+recentchanges-paramvalue-prop-comment": "يضيف التعليق للتحرير.",
+       "apihelp-query+recentchanges-paramvalue-prop-parsedcomment": "يضيف التعليق المحلل للتحرير.",
+       "apihelp-query+recentchanges-paramvalue-prop-flags": "يضيف علامات للتحرير.",
+       "apihelp-query+recentchanges-paramvalue-prop-timestamp": "يضيف الطابع الزمني للتحرير.",
+       "apihelp-query+recentchanges-paramvalue-prop-title": "يضيف عنوان صفحة التحرير.",
+       "apihelp-query+recentchanges-paramvalue-prop-ids": "يضيف معرف الصفحة ومعرف أحدث التغييرات ومعرف النسخة الجديدة والقديمة.",
+       "apihelp-query+recentchanges-paramvalue-prop-sizes": "يضيف طول الصفحة الجديد والقديم بالبايت.",
+       "apihelp-query+recentchanges-paramvalue-prop-redirect": "يوسم التحرير إذا كانت الصفحة تحويلة",
+       "apihelp-query+recentchanges-paramvalue-prop-patrolled": "يوسم التعديلات التي يمكن مراجعتها باعتبارها مراجعة أو غير مراجعة.",
+       "apihelp-query+recentchanges-paramvalue-prop-autopatrolled": "يوسم التعديلات التي يمكن مراجعتها باعتبارها مراجعة تلقائيا أم لا.",
+       "apihelp-query+recentchanges-paramvalue-prop-loginfo": "يضيف معلومات السجل (معرف السجل، نوع السجل، إلخ) لإدخالات السجل.",
+       "apihelp-query+recentchanges-paramvalue-prop-tags": "يسرد الوسوم للدخول.",
+       "apihelp-query+recentchanges-paramvalue-prop-sha1": "يضيف المجموع الاختباري للمحتوى للإدخالات المرتبطة بمراجعة.",
+       "apihelp-query+recentchanges-param-token": "استخدم [[Special:ApiHelp/query+tokens|action=query&meta=tokens]] بدلا من ذلك.",
+       "apihelp-query+recentchanges-param-show": "إظهار العناصر التي تستوفي هذه المعايير فقط، على سبيل المثال، مشاهدة التعديلات الطفيفة فقط التي قام بها المستخدمون مسجلو الدخول، قم بتعيين $1show=minor|!anon.",
+       "apihelp-query+recentchanges-param-limit": "كم عدد التغييرات الإجمالي للعودة.",
+       "apihelp-query+recentchanges-param-type": "أي أنواع التغييرات لعرضها.",
+       "apihelp-query+recentchanges-param-toponly": "سرد التغييرات التي هي أحدث مراجعة فقط.",
+       "apihelp-query+recentchanges-param-title": "تصفية الإدخالات إلى تلك المتعلقة بصفحة.",
+       "apihelp-query+recentchanges-param-generaterevisions": "عند استخدامه كمولد، قم بإنشاء معرفات المراجعة بدلا من العناوين، ولن تؤدي إدخالات التغيير الأخيرة التي لا تحتوي على معرفات المراجعة المرتبطة (مثل معظم إدخالات السجلات) إلى توليد أي شيء.",
+       "apihelp-query+recentchanges-example-simple": "سرد أحدث التغييرات.",
+       "apihelp-query+recentchanges-example-generator": "الحصول على معلومات الصفحة حول أحدث التغييرات غير المراجعة.",
+       "apihelp-query+redirects-summary": "يعرض جميعالتحويلات إلى الصفحات المحددة.",
        "apihelp-query+redirects-param-prop": "أي الخصائص للحصول عليها.",
+       "apihelp-query+redirects-paramvalue-prop-pageid": "معرف الصفحة لكل تحويلة.",
+       "apihelp-query+redirects-paramvalue-prop-title": "عنوان كل تحويلة.",
+       "apihelp-query+redirects-paramvalue-prop-fragment": "جزء من كل تحويلة، إن وُجِدت.",
+       "apihelp-query+redirects-param-namespace": "إدراج الصفحات في هذه النطاقات فقط.",
+       "apihelp-query+redirects-param-limit": "كم عدد التحويلات لإرجاعها.",
+       "apihelp-query+redirects-param-show": "إظهار العناصر التي تستوفي هذه المعايير فقط:\n;fragment:إظهار التحويلات بجزء فقط.\n;!fragment:إظهار التحويلات بدون جزء فقط.",
+       "apihelp-query+redirects-example-simple": "احصل على قائمة بالتحويلات إلى [[Main Page]].",
+       "apihelp-query+redirects-example-generator": "احصل على معلومات حول جميع التحويلات إلى [[Main Page]].",
+       "apihelp-query+revisions-summary": "الحصول على معلومات المراجعة.",
+       "apihelp-query+revisions-extended-description": "يمكن استخدامه بعدة طرق: \n# الحصول على بيانات حول مجموعة من الصفحات (المراجعة الأخيرة)، عن طريق تعيين عناوين أو معرفات صفحات. \n# احصل على مراجعات لصفحة معينة، باستخدام العناوين أو معرفات الصفحات ذات البداية، أو النهاية، أو الحد. \n# الحصول على بيانات حول مجموعة من المراجعات من خلال تعيين معرفاتها مع معرفات المراجعات.",
+       "apihelp-query+revisions-paraminfo-singlepageonly": "لا يجوز استخدامها إلا مع صفحة واحدة (الوضع #2).",
+       "apihelp-query+revisions-param-startid": "بدء التعداد من الطابع الزمني لهذه المراجعة، يجب أن تكون النسخة موجودة، لكن لا يجب أن تنتمي إلى هذه الصفحة.",
+       "apihelp-query+revisions-param-endid": "إيقاف التعداد في الطابع الزمني لهذه المراجعة، يجب أن تكون النسخة موجودة، لكن لا يجب أن تنتمي إلى هذه الصفحة.",
+       "apihelp-query+revisions-param-start": "من أي طابع زمني للمراجعة لبدء التعداد.",
+       "apihelp-query+revisions-param-end": "تعداد يصل إلى هذا الطابع الزمني.",
+       "apihelp-query+revisions-param-user": "عدم تضمين سوى المراجعات التي أجراها المستخدم.",
+       "apihelp-query+revisions-param-excludeuser": "استبعاد المراجعات التي أجراها المستخدم.",
        "apihelp-query+revisions-param-tag": "إدراج المراجعات الموسومة بهذ الوسم فقط.",
+       "apihelp-query+revisions-param-token": "أي الرموز المميزة للحصول عليها لكل مراجعة.",
+       "apihelp-query+revisions-example-content": "احصل على بيانات تتضمن محتوى آخر مراجعة لـ<kbd>API</kbd> العناوين و<kbd>Main Page</kbd>.",
+       "apihelp-query+revisions-example-last5": "احصل على آخر 5 مراجعات لـ<kbd>Main Page</kbd>.",
+       "apihelp-query+revisions-example-first5": "احصل على أول 5 مراجعات من <kbd>Main Page</kbd>.",
+       "apihelp-query+revisions-example-first5-after": "احصل على أول 5 مراجعات للصفحة <kbd>Main Page</kbd> بعد 2006-05-01.",
+       "apihelp-query+revisions-example-first5-not-localhost": "احصل على أول 5 مراجعات للصفحة <kbd>Main Page</kbd> التي لم يجرها المستخدم المجهول <kbd>127.0.0.1</kbd>.",
+       "apihelp-query+revisions-example-first5-user": "احصل على أول 5 مراجعات للصفحة <kbd>Main Page</kbd> التي تم إجراؤها بواسطة المستخدم <kbd>MediaWiki default</kbd>.",
+       "apihelp-query+revisions+base-param-prop": "أي الخصائص للحصول عليها لكل مراجعة:",
+       "apihelp-query+revisions+base-paramvalue-prop-ids": "معرف المراجعة.",
+       "apihelp-query+revisions+base-paramvalue-prop-flags": "أعلام المراجعة (طفيفة).",
+       "apihelp-query+revisions+base-paramvalue-prop-timestamp": "الطابع الزمني للمراجعة.",
+       "apihelp-query+revisions+base-paramvalue-prop-user": "المستخدم الذي أجرى المراجعة.",
+       "apihelp-query+revisions+base-paramvalue-prop-userid": "معرف المستخدم لمنشئ المراجعة.",
+       "apihelp-query+revisions+base-paramvalue-prop-size": "طول (بايت) المراجعة.",
+       "apihelp-query+revisions+base-paramvalue-prop-sha1": "SHA-1 (القاعدة 16) المراجعة.",
+       "apihelp-query+revisions+base-paramvalue-prop-contentmodel": "معرف نموذج المحتوى للمراجعة.",
+       "apihelp-query+revisions+base-paramvalue-prop-comment": "تعليق من قبل المستخدم للمراجعة.",
+       "apihelp-query+revisions+base-paramvalue-prop-parsedcomment": "تعليق محلل من قبل المستخدم للمراجعة.",
+       "apihelp-query+revisions+base-paramvalue-prop-content": "نص المراجعة.",
+       "apihelp-query+revisions+base-paramvalue-prop-tags": "وسوم للمراجعة.",
+       "apihelp-query+revisions+base-paramvalue-prop-parsetree": "<span class=\"apihelp-deprecated\">موقوف.</span> استخدم <kbd>[[Special:ApiHelp/expandtemplates|action=expandtemplates]]</kbd> أو <kbd>[[Special:ApiHelp/parse|action=parse]]</kbd> بدلا من ذلك، شجرة تحليل XML لمحتوى المراجعة (تتطلب نموذج المحتوى <code>$1</code>).",
+       "apihelp-query+revisions+base-param-limit": "الحد من عدد المراجعات التي سيتم إرجاعها.",
+       "apihelp-query+revisions+base-param-expandtemplates": "استخدم <kbd>[[Special:ApiHelp/expandtemplates|action=expandtemplates]]</kbd> بدلا من ذلك، قم بتوسيع القوالب في محتوى المراجعة (يتطلب $1prop=content).",
+       "apihelp-query+revisions+base-param-generatexml": "استخدم <kbd>[[Special:ApiHelp/expandtemplates|action=expandtemplates]]</kbd> أو <kbd>[[Special:ApiHelp/parse|action=parse]]</kbd> بدلا من ذلك، قم بتوليد شجرة تحليل XML لمحتوى المراجعة (تتطلب $1prop=content).",
+       "apihelp-query+revisions+base-param-parse": "استخدم <kbd>[[Special:ApiHelp/parse|action=parse]]</kbd> بدلا من ذلك، تحليل محتوى المراجعة (يتطلب $1prop=content)، لأسباب تتعلق بالأداء; إذا تم استخدام هذا الخيار، يتم فرض $1limit إلى 1.",
+       "apihelp-query+revisions+base-param-section": "استرجع محتويات رقم هذا القسم فقط.",
+       "apihelp-query+revisions+base-param-difftotextpst": "استخدم <kbd>[[Special:ApiHelp/compare|action=compare]]</kbd> بدلا من ذلك، قم بإجراء تحويل ما قبل الحفظ على النص قبل نشره، صالح فقط عند استخدامه مع <var>$1difftotext</var>.",
+       "apihelp-query+revisions+base-param-contentformat": "تنسيق التسلسل المستخدم لـ<var>$1difftotext</var> والمتوقع لإخراج المحتوى.",
+       "apihelp-query+search-summary": "إجراء بحث نص كامل.",
+       "apihelp-query+search-param-search": "ابحث عن عناوين الصفحات أو المحتوى الذي يطابق هذه القيمة، يمكنك استخدام سلسلة البحث لاستدعاء ميزات بحث خاصة، اعتمادا على ما تنفذه الواجهة الخلفية للبحث في موقع الويكي.",
+       "apihelp-query+search-param-namespace": "ابحث داخل هذه النطاقات فقط.",
+       "apihelp-query+search-param-what": "أي نوع من البحث لأدائه.",
+       "apihelp-query+search-param-info": "أية بيانات وصفية لعرضها.",
+       "apihelp-query+search-param-prop": "أي الخصائص للعودة.",
+       "apihelp-query+search-param-qiprofile": "الاستعلام عن ملف شخصي مستقل للاستخدام (يؤثر على خوارزمية الترتيب).",
+       "apihelp-query+search-paramvalue-prop-size": "يضيف حجم الصفحة بالبايت.",
+       "apihelp-query+search-paramvalue-prop-wordcount": "يضيف عدد كلمات الصفحة.",
+       "apihelp-query+search-paramvalue-prop-timestamp": "يضيف الطابع الزمني لوقت آخر تعديل للصفحة.",
+       "apihelp-query+search-paramvalue-prop-snippet": "يضيف مقتطفا محللا للصفحة.",
+       "apihelp-query+search-paramvalue-prop-titlesnippet": "يضيف مقتطفا محللا لعنوان الصفحة.",
+       "apihelp-query+search-paramvalue-prop-redirectsnippet": "يضيف مقتطفا محللا لعنوان التحويلة.",
+       "apihelp-query+search-paramvalue-prop-redirecttitle": "يضيف عنوان التحويلة المطابقة.",
+       "apihelp-query+search-paramvalue-prop-sectionsnippet": "يضيف مقتطفا محللا لعنوان القسم المطابق.",
+       "apihelp-query+search-paramvalue-prop-sectiontitle": "يضيف عنوان القسم المطابق.",
+       "apihelp-query+search-paramvalue-prop-categorysnippet": "يضيف مقتطفا محللا للتصنيف المطابق.",
+       "apihelp-query+search-paramvalue-prop-isfilematch": "يضيف قيمة منطقية تشير إلى ما إذا كان محتوى البحث مطابقا للمحتوى.",
+       "apihelp-query+search-paramvalue-prop-extensiondata": "يضيف بيانات إضافية منشأة بواسطة الإضافات.",
+       "apihelp-query+search-paramvalue-prop-score": "تم تجاهله.",
+       "apihelp-query+search-paramvalue-prop-hasrelated": "تم تجاهله.",
        "apihelp-query+search-param-limit": "كم عدد مجموع الصفحات للعودة.",
+       "apihelp-query+search-param-interwiki": "تضمين نتائج الإنترويكي في البحث، إذا كان ذلك متاحا.",
+       "apihelp-query+search-param-backend": "أية واجهة خلفية للبحث مستخدمة، إن لم تكن الافتراضية.",
+       "apihelp-query+search-param-enablerewrites": "تمكين إعادة كتابة الاستعلام الداخلية، يمكن لبعض الواجهات الخلفية البحث إعادة كتابة الاستعلام إلى آخر يُعتَقد أنه يوفر نتائج أفضل، على سبيل المثال عن طريق تصحيح الأخطاء الإملائية.",
+       "apihelp-query+search-param-sort": "تعيين ترتيب الفرز للنتائج التي تم إرجاعها.",
+       "apihelp-query+search-example-simple": "البحث عن <kbd>meaning</kbd>.",
+       "apihelp-query+search-example-text": "البحث في النصوص عن <kbd>meaning</kbd>.",
+       "apihelp-query+search-example-generator": "احصل على معلومات الصفحة حول الصفحات التي تم إرجاعها للبحث عن <kbd>meaning</kbd>.",
+       "apihelp-query+siteinfo-summary": "إرجاع معلومات عامة حول الموقع.",
+       "apihelp-query+siteinfo-param-prop": "أي المعلومات للحصول عليها:",
+       "apihelp-query+siteinfo-paramvalue-prop-general": "معلومات النظام الشاملة.",
+       "apihelp-query+siteinfo-paramvalue-prop-namespaces": "قائمة النطاقات المسجلة وأسمائها الأساسية.",
+       "apihelp-query+siteinfo-paramvalue-prop-namespacealiases": "قائمة الاسماء المستعارة المسجلة للنطاقات.",
+       "apihelp-query+siteinfo-paramvalue-prop-specialpagealiases": "قائمة الأسماء المستعارة للصفحات الخاصة.",
+       "apihelp-query+siteinfo-paramvalue-prop-magicwords": "قائمة الكلمات السحرية وأسمائها المستعارة.",
+       "apihelp-query+siteinfo-paramvalue-prop-statistics": "يعيد إحصائيات الموقع.",
+       "apihelp-query+siteinfo-paramvalue-prop-interwikimap": "يرجع خريطة الإنترويكي (يتم ترشيحها اختياريا، ويتم اختيارها اختياريا باستخدام <var>$1inlanguagecode</var>).",
+       "apihelp-query+siteinfo-paramvalue-prop-dbrepllag": "يرجع خادم قاعدة البيانات مع تأخر النسخ المتماثل الأعلى.",
+       "apihelp-query+siteinfo-paramvalue-prop-usergroups": "يرجع مجموعات المستخدمين والصلاحيات المرتبطة.",
+       "apihelp-query+siteinfo-paramvalue-prop-libraries": "يرجع المكتبات المثبتة على الويكي.",
+       "apihelp-query+siteinfo-paramvalue-prop-extensions": "يرجع الإضافات المثبتة على الويكي.",
+       "apihelp-query+siteinfo-paramvalue-prop-fileextensions": "يرجع قائمة امتدادات الملفات (أنواع الملفات) المسموح برفعها.",
+       "apihelp-query+siteinfo-paramvalue-prop-rightsinfo": "يرجع معلومات حقوق (ترخيص) الويكي إن كانت متاحة.",
+       "apihelp-query+siteinfo-paramvalue-prop-restrictions": "يرجع المعلومات حول أنواع القيود (الحماية) المتاحة.",
+       "apihelp-query+siteinfo-paramvalue-prop-languages": "يعرض قائمة اللغات التي يدعمها ميدياويكي (مترجمة اختياريا باستخدام <var>$1inlanguagecode</var>).",
+       "apihelp-query+siteinfo-paramvalue-prop-languagevariants": "يعرض قائمة بأكواد اللغات التي يتم تمكين [[mw:Special:MyLanguage/LanguageConverter|LanguageConverter]] بها، والمتغيرات المدعومة المختلفة لكل منها.",
+       "apihelp-query+siteinfo-paramvalue-prop-skins": "يعرض قائمة بجميع المظاهر الممكَّنة (مترجمة اختياريا باستخدام <var>$1inlanguagecode</var>، بخلاف لغة المحتوى).",
+       "apihelp-query+siteinfo-paramvalue-prop-extensiontags": "يعرض قائمة وسوم امتداد المحلل.",
+       "apihelp-query+siteinfo-paramvalue-prop-functionhooks": "يرجع قائمة خطاطيف دالة المحلل.",
+       "apihelp-query+siteinfo-paramvalue-prop-showhooks": "يعرض قائمة بكل الخطاطيف المشتركة (محتويات <var>[[mw:Special:MyLanguage/Manual:$wgHooks|$wgHooks]]</var>).",
+       "apihelp-query+siteinfo-paramvalue-prop-variables": "يعرض قائمة بمعرفات المتغيرات.",
+       "apihelp-query+siteinfo-paramvalue-prop-protocols": "يعرض قائمة بالبروتوكولات المسموح بها في الروابط الخارجية.",
+       "apihelp-query+siteinfo-paramvalue-prop-defaultoptions": "يعرض القيم الافتراضية لتفضيلات المستخدم.",
+       "apihelp-query+siteinfo-paramvalue-prop-uploaddialog": "يعرض تكوين مربع حوار الرفع.",
+       "apihelp-query+siteinfo-param-filteriw": "إرجاع الإدخالات المحلية أو غير المحلية فقط بخريطة الإنترويكي.",
+       "apihelp-query+siteinfo-param-showalldb": "سرد جميع خوادم قواعد البيانات، وليست فقط واحدة تخلفت أكثر.",
+       "apihelp-query+siteinfo-param-numberingroup": "يسرد عدد المستخدمين في مجموعات المستخدمين.",
+       "apihelp-query+siteinfo-param-inlanguagecode": "رمز اللغة لأسماء اللغة المترجمة (أفضل جهد) وأسماء المظاهر.",
+       "apihelp-query+siteinfo-example-simple": "إحضار معلومات الموقع.",
+       "apihelp-query+siteinfo-example-interwiki": "إحضار قائمة ببادئات الإنترويكي المحلية.",
+       "apihelp-query+siteinfo-example-replag": "تحقق من تأخر النسخ المتماثل الحالي.",
+       "apihelp-query+stashimageinfo-summary": "يرجع معلومات الملف للملفات المملوءة.",
+       "apihelp-query+stashimageinfo-param-filekey": "المفتاح الذي يحدد التحميل السابق المخزن مؤقتا.",
+       "apihelp-query+stashimageinfo-param-sessionkey": "الاسم المستعار لـ$1filekey; للتوافق مع الإصدارات السابقة.",
+       "apihelp-query+stashimageinfo-example-simple": "يرجع معلومات لملف مملوء.",
+       "apihelp-query+stashimageinfo-example-params": "يرجع الصور المصغرة لملفين مخزنين.",
+       "apihelp-query+tags-summary": "سرد وسوم التغيير.",
+       "apihelp-query+tags-param-limit": "الحد الأقصى لعدد الوسوم لإدراجها.",
        "apihelp-query+tags-param-prop": "أي الخصائص للحصول عليها.",
+       "apihelp-query+tags-paramvalue-prop-name": "يضيف اسم الوسم.",
+       "apihelp-query+tags-paramvalue-prop-displayname": "يضيف رسالة نظام للوسم.",
+       "apihelp-query+tags-paramvalue-prop-description": "يضيف وصف الوسم.",
+       "apihelp-query+tags-paramvalue-prop-hitcount": "يضيف عدد المراجعات وإدخالات السجلات التي تحتوي على هذا الوسم.",
+       "apihelp-query+tags-paramvalue-prop-defined": "حدد ما إذا كانت الوسم محددا.",
+       "apihelp-query+tags-paramvalue-prop-source": "الحصول على مصادر الوسم، والتي قد تتضمن <samp>extension</samp>  للوسوم المعرفة بالامتداد و<samp>manual</samp> للوسوم التي قد يتم تطبيقها يدويا من قبل المستخدمين.",
+       "apihelp-query+tags-paramvalue-prop-active": "ما إذا كان الوسم لا يزال قيد التطبيق.",
+       "apihelp-query+tags-example-simple": "سرد الوسوم المتاحة.",
+       "apihelp-query+templates-summary": "يعرض جميع الملفات المضمنة في الصفحات المعينة.",
+       "apihelp-query+templates-param-namespace": "إظهار القوالب في هذه النطاقات فقط.",
+       "apihelp-query+templates-param-limit": "كم عدد القوالب للعودة.",
+       "apihelp-query+templates-param-templates": "إدراج هذه القوالب فقط، مفيد للتحقق ما إذا كانت صفحة معينة تستخدم قالبا معينا.",
        "apihelp-query+templates-param-dir": "الاتجاه للإدراج فيه.",
+       "apihelp-query+templates-example-simple": "احصل على القوالب المستخدمة في الصفحة <kbd>Main Page</kbd>.",
+       "apihelp-query+templates-example-generator": "احصل على معلومات حول صفحات القوالب المستخدمة في <kbd>Main Page</kbd>.",
+       "apihelp-query+templates-example-namespaces": "احصل على صفحات في نطاقي {{ns:user}} و{{ns:template}} المضمنة في الصفحة <kbd>Main Page</kbd>.",
+       "apihelp-query+tokens-summary": "الحصول على الرموز المميزة لإجراءات تعديل البيانات.",
+       "apihelp-query+tokens-param-type": "أنواع الرمز المميز للطلب.",
+       "apihelp-query+tokens-example-simple": "استرداد رمز csrf (الافتراضي).",
+       "apihelp-query+tokens-example-types": "استرجع رمز مراقبة ورمز مراجعة.",
+       "apihelp-query+transcludedin-summary": "ابحث عن جميع الصفحات التي تتضمن الصفحات المعينة.",
        "apihelp-query+transcludedin-param-prop": "أي الخصائص للحصول عليها.",
+       "apihelp-query+transcludedin-paramvalue-prop-pageid": "معرف الصفحة لكل صفحة.",
+       "apihelp-query+transcludedin-paramvalue-prop-title": "عنوان كل صفحة.",
+       "apihelp-query+transcludedin-paramvalue-prop-redirect": "علم إذا كانت الصفحة تحويلة.",
+       "apihelp-query+transcludedin-param-namespace": "إدراج الصفحات في هذه النطاقات فقط.",
+       "apihelp-query+transcludedin-param-limit": "كم العدد للعودة.",
+       "apihelp-query+transcludedin-param-show": "إظهار العناصر التي تستوفي هذه المعايير فقط:\n;تحويلة: عرض التحويلات فقط.\n;!تحويلة:إظهار غير التحويلات فقط.",
+       "apihelp-query+transcludedin-example-simple": "احصل على قائمة بالصفحات التي تتضمن <kbd>Main Page</kbd>.",
+       "apihelp-query+transcludedin-example-generator": "الحصول على معلومات حول الصفحات التي تتضمن <kbd>Main Page</kbd>.",
+       "apihelp-query+usercontribs-summary": "الحصول على جميع التعديلات من قبل المستخدم.",
+       "apihelp-query+usercontribs-param-limit": "الحد الأقصى لعدد المساهمات للعودة.",
+       "apihelp-query+usercontribs-param-start": "الطابع الزمني للبدء للعودة منه.",
+       "apihelp-query+usercontribs-param-prop": "تضمين أجزاء إضافية من المعلومات:",
        "apihelp-query+usercontribs-paramvalue-prop-title": "يضيف معرف عنوان ونطاق الصفحة.",
        "apihelp-query+usercontribs-param-tag": "إدراج المراجعات الموسومة بهذ الوسم فقط.",
+       "apihelp-query+usercontribs-param-toponly": "سرد التغييرات التي هي أحدث مراجعة فقط.",
        "apihelp-query+userinfo-param-prop": "أية قطعة من المعلومات لتضمينها:",
        "apihelp-query+users-param-prop": "أية قطعة من المعلومات لتضمينها:",
        "apihelp-query+watchlist-param-start": "الطابع الزمني لبدء العد منه.",
+       "apihelp-query+watchlist-param-end": "الطابع الزمني لإنهاء التعداد.",
+       "apihelp-query+watchlist-param-type": "أي أنواع التغييرات لعرضها.",
+       "apihelp-tokens-summary": "الحصول على الرموز المميزة لإجراءات تعديل البيانات.",
+       "apihelp-tokens-param-type": "أنواع الرمز المميز للطلب.",
        "apierror-offline": "لم يمكن المتابعة بسبب مشاكل في الاتصال بالشبكة; تأكد من أنه لديك اتصال بالإنترنت وحاول مرة أخرى.",
        "apierror-timeout": "لم يستجب الخادم ضمن الوقت المتوقع.",
        "api-feed-error-title": "خطأ ($1)"
index 13d392e..d0e9f1a 100644 (file)
        "apihelp-query+search-paramvalue-prop-wordcount": "Ergänzt den Wortzähler der Seite.",
        "apihelp-query+search-paramvalue-prop-extensiondata": "Ergänzt zusätzliche von Erweiterungen erzeugte Daten.",
        "apihelp-query+search-param-limit": "Wie viele Seiten insgesamt zurückgegeben werden sollen.",
+       "apihelp-query+search-param-sort": "Legt die Sortierreihenfolge der zurückgegebenen Ergebnisse fest.",
        "apihelp-query+search-example-simple": "Nach <kbd>meaning</kbd> suchen.",
        "apihelp-query+search-example-text": "Texte nach <kbd>meaning</kbd> durchsuchen.",
        "apihelp-query+siteinfo-paramvalue-prop-specialpagealiases": "Liste von Spezialseiten-Aliasse.",
index e1ba665..94e1ac6 100644 (file)
        "apihelp-query+search-param-interwiki": "Include interwiki results in the search, if available.",
        "apihelp-query+search-param-backend": "Which search backend to use, if not the default.",
        "apihelp-query+search-param-enablerewrites": "Enable internal query rewriting. Some search backends can rewrite the query into another which is thought to provide better results, for instance by correcting spelling errors.",
+       "apihelp-query+search-param-sort": "Set the sort order of returned results.",
        "apihelp-query+search-example-simple": "Search for <kbd>meaning</kbd>.",
        "apihelp-query+search-example-text": "Search texts for <kbd>meaning</kbd>.",
        "apihelp-query+search-example-generator": "Get page info about the pages returned for a search for <kbd>meaning</kbd>.",
index 37ee993..323a784 100644 (file)
        "apihelp-query+search-param-interwiki": "Inclure les résultats interwiki dans la recherche, s’ils sont disponibles.",
        "apihelp-query+search-param-backend": "Quel serveur de recherche utiliser, si ce n’est pas celui par défaut.",
        "apihelp-query+search-param-enablerewrites": "Activer la réécriture interne de la requête. Les serveurs de recherche peuvent réécrire la requête en une autre qui est censée donner de meilleurs résultats, par exemple en corrigeant les erreurs d’orthographe.",
+       "apihelp-query+search-param-sort": "Fixe l’ordre de tri des résultats renvoyés.",
        "apihelp-query+search-example-simple": "Rechercher <kbd>meaning</kbd>.",
        "apihelp-query+search-example-text": "Rechercher des textes pour <kbd>meaning</kbd>.",
        "apihelp-query+search-example-generator": "Obtenir les informations sur les pages renvoyées par une recherche de <kbd>meaning</kbd>.",
index b53c052..057adc0 100644 (file)
        "apihelp-query+search-param-interwiki": "לכלול תוצאות בינוויקי בחיפוש, אם זמין.",
        "apihelp-query+search-param-backend": "באיזה שרת חיפוש להשתמש אם לא בבררת המחדל.",
        "apihelp-query+search-param-enablerewrites": "הפעלת שכתוב שאילתות פנימי. שרתי חיפוש אחדים יכולים לשכתב את השאילתה לצורה אחרת שנחשבת לכזאת שמספקת תוצאות טובות יותר, למשל באמצעות תיקון שגיאות כתיב.",
+       "apihelp-query+search-param-sort": "קביעת סדר המיון של התוצאות המוחזרות.",
        "apihelp-query+search-example-simple": "חיפוש <kbd>meaning</kbd>.",
        "apihelp-query+search-example-text": "חיפוש טקסטים עבור <kbd>meaning</kbd>.",
        "apihelp-query+search-example-generator": "קבלת מידע על הדף עבור שמוחזרים מחיפוש אחרי <kbd>meaning</kbd>.",
index 575715f..2bcaef1 100644 (file)
        "apihelp-query+search-paramvalue-prop-score": "Ignorato.",
        "apihelp-query+search-paramvalue-prop-hasrelated": "Ignorato.",
        "apihelp-query+search-param-limit": "Quante pagine totali restituire.",
+       "apihelp-query+search-param-sort": "Imposta l'ordinamento dei risultati restituiti.",
        "apihelp-query+siteinfo-param-prop": "Quali informazioni ottenere:",
        "apihelp-query+siteinfo-paramvalue-prop-statistics": "Restituisce le statistiche del sito.",
        "apihelp-query+siteinfo-paramvalue-prop-libraries": "Restituisci librerie installate sul wiki.",
index f88bd90..c486045 100644 (file)
        "apihelp-edit-param-watch": "문서를 현재 사용자의 주시문서 목록에 추가합니다.",
        "apihelp-edit-param-unwatch": "문서를 현재 사용자의 주시문서 목록에서 제거합니다.",
        "apihelp-edit-param-watchlist": "현재 사용자의 주시목록에서 문서를 무조건적으로 추가하거나 제거하거나, 환경 설정을 사용하거나 주시를 변경하지 않습니다.",
-       "apihelp-edit-param-redirect": "자동으로 넘겨주기 처리하기.",
+       "apihelp-edit-param-prependtext": "이 텍스를 문서의 처음에 추가합니다. $1text를 무효로 합니다.",
+       "apihelp-edit-param-appendtext": "이 텍스트를 문서의 끝에 추가합니다. $1text를 무효로 합니다.\n\n새 문단을 추가하려면 이 변수 대신 $1section=new를 사용하십시오.",
+       "apihelp-edit-param-undo": "이 판의 편집을 취소합니다. $1text, $1prependtext, $1appendtext를 무효로 합니다.",
+       "apihelp-edit-param-undoafter": "$1undo에서부터 이 판까지의 모든 판의 편집을 취소합니다. 설정하지 않으면 하나의 판만 편집을 취소합니다.",
+       "apihelp-edit-param-redirect": "자동으로 넘겨주기를 처리합니다.",
+       "apihelp-edit-param-contentformat": "입력 텍스트에 사용할 내용 직렬화 포맷입니다.",
        "apihelp-edit-param-contentmodel": "새 콘텐츠의 콘텐츠 모델.",
+       "apihelp-edit-param-token": "토큰은 무조건 마지막 변수로 보내거나 적어도 $1text 변수 뒤에 보내는 것이 좋습니다.",
        "apihelp-edit-example-edit": "문서 편집",
+       "apihelp-edit-example-prepend": "문서의 맨 앞에 <kbd>_&#95;NOTOC_&#95;</kbd>를 추가합니다.",
        "apihelp-edit-example-undo": "자동 편집요약으로 13579판에서 13585판까지 되돌리기.",
        "apihelp-emailuser-summary": "사용자에게 이메일을 보냅니다.",
        "apihelp-emailuser-param-target": "이메일을 받을 사용자.",
        "apihelp-parse-param-sectionpreview": "문단 미리 보기 모드에서 구문 분석을 합니다. (미리 보기 모드도 활성화함)",
        "apihelp-parse-param-disabletoc": "출력에서 목차를 제외합니다.",
        "apihelp-parse-param-useskin": "선택한 스킨을 파서 출력에 적용합니다. 다음의 속성에 영향을 줄 수 있습니다: <kbd>langlinks</kbd>, <kbd>headitems</kbd>, <kbd>modules</kbd>, <kbd>jsconfigvars</kbd>, <kbd>indicators</kbd>.",
+       "apihelp-parse-param-contentformat": "입력 텍스트에 사용할 내용 직렬화 포맷입니다. $1text와 함께 사용할 때에만 유효합니다.",
        "apihelp-parse-example-page": "페이지의 구문을 분석합니다.",
        "apihelp-parse-example-text": "위키텍스트의 구문을 분석합니다.",
        "apihelp-parse-example-summary": "요약을 구문 분석합니다.",
        "apihelp-query+search-paramvalue-prop-timestamp": "문서가 마지막으로 편집된 시기의 타임스탬프를 추가합니다.",
        "apihelp-query+search-paramvalue-prop-score": "무시됨.",
        "apihelp-query+search-paramvalue-prop-hasrelated": "무시됨.",
+       "apihelp-query+search-param-sort": "반환된 결과의 정렬 순서를 설정합니다.",
        "apihelp-query+search-example-simple": "<kbd>meaning</kbd>을 검색합니다.",
        "apihelp-query+search-example-text": "<kbd>meaning</kbd>의 텍스트를 검색합니다.",
        "apihelp-query+siteinfo-summary": "사이트의 전반적인 정보를 반환합니다.",
        "apihelp-stashedit-param-sectiontitle": "새 문단을 위한 제목.",
        "apihelp-stashedit-param-text": "문서 내용.",
        "apihelp-stashedit-param-contentmodel": "새 콘텐츠의 콘텐츠 모델.",
+       "apihelp-stashedit-param-contentformat": "입력 텍스트에 사용할 내용 직렬화 포맷입니다.",
        "apihelp-tag-summary": "개별 판이나 기록 항목에서 변경 태그를 추가하거나 제거합니다.",
        "apihelp-tag-param-rcid": "태그를 변경하거나 추가할 하나 이상의 최근 바뀜 ID입니다.",
        "apihelp-tag-param-revid": "태그를 추가하거나 제거할 하나 이상의 판 ID입니다.",
        "api-help-datatypes-header": "데이터 유형",
        "api-help-datatypes": "API 요청 내 몇몇 매개변수형에 대해 더 자세히 설명해보겠습니다:\n;boolean\n:Boolean 매개변수들은 HTML 체크박스처럼 동작합니다: 만약 매개변수가 지정되었다면, 값에 상관없이 참의 값으로 여겨집니다. 거짓값은 매개변수 전체를 생략하세요.\n;timestamp\n:타임스탬프들은 여러 형식으로 표현될 수 있으나 ISO 8601 날짜와 시간이 추천됩니다. 모든 시간은 UTC이어야 하며, 포함된 시간대는 모두 무시됩니다.\n:* ISO 8601 날짜와 시간, <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>Z</kbd> (구두점과 <kbd>Z</kbd>는 선택입니다.)\n:* ISO 8601 날짜와 시간과 (무시되는) 소수 초, <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>.<var>00001</var>Z</kbd> (대시, 콜론과 <kbd>Z</kbd>는 선택입니다.)\n:* 미디어위키 형식, <kbd><var>2001</var><var>01</var><var>15</var><var>14</var><var>56</var><var>00</var></kbd>\n:* 일반적인 수 형식 <kbd><var>2001</var>-<var>01</var>-<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd> (<kbd>GMT</kbd>, <kbd>+<var>##</var></kbd>, 또는 <kbd>-<var>##</var></kbd>와 같은 선택적 시간대는 무시됩니다)\n:*RFC 2822 형식 (시간대는 생략될 수 있음), <kbd><var>Mon</var>, <var>15</var> <var>Jan</var> <var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* RFC 850 형식 (시간대는 생략될 수 있음), <kbd><var>Monday</var>, <var>15</var>-<var>Jan</var>-<var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* C ctime 형식, <kbd><var>Mon</var> <var>Jan</var> <var>15</var> <var>14</var>:<var>56</var>:<var>00</var> <var>2001</var></kbd>\n:* 1부터 13자리까지의 숫자로 표현된 1970-01-01T00:00:00Z부터 흐른 시간(초) (<kbd>0</kbd>을 제외)\n:* 문자열 <kbd>now</kbd>",
        "api-help-templatedparams-header": "틀 변수",
+       "api-help-templatedparams": "틀 변수는 다른 일부 변수의 개별 값에 대한 API 모듈에 값이 필요한 경우를 지원합니다. 이를테면 과일을 요청하는 API 모듈이 있다면 <var>fruits</var> 변수를 사용하여 요청할 과일을 지정할 수 있으며 틀 변수 <var>{fruit}-quantity</var>를 사용하여 요청할 과일의 수를 지정할 수 있습니다. 사과 1개, 바나나 5개, 딸기 20개를 원하는 API 클라이언트는 <kbd>fruits=apples|bananas|strawberries&apples-quantity=1&bananas-quantity=5&strawberries-quantity=20</kbd>와 같은 요청을 수행할 수 있습니다.",
        "api-help-param-type-limit": "유형: 정수 또는 <kbd>max</kbd>",
        "api-help-param-type-integer": "유형: {{PLURAL:$1|1=정수|2=정수 목록}}",
        "api-help-param-type-boolean": "유형: 불리언 ([[Special:ApiHelp/main#main/datatypes|자세한 정보]])",
        "api-help-param-type-timestamp": "유형: {{PLURAL:$1|1=타임스탬프|2=타임스탬프 목록}} ([[Special:ApiHelp/main#main/datatypes|허용되는 포맷]])",
        "api-help-param-type-user": "유형: {{PLURAL:$1|1=사용자 이름|2=사용자 이름 목록}}",
-       "api-help-param-list": "{{PLURAL:$1|1=다음 값 중 하나|2=값 (<kbd>{{!}}</kbd>로 구분)}}: $2 또는 [[Special:ApiHelp/main#main/datatypes|alternative]]: $2",
+       "api-help-param-list": "{{PLURAL:$1|1=다음 값 중 하나|2=값 (<kbd>{{!}}</kbd>로 구분)}}: $2 또는 [[Special:ApiHelp/main#main/datatypes|다른 문자열]]: $2",
        "api-help-param-list-can-be-empty": "{{PLURAL:$1|0=비어 있어야 함|비어 있을 수 있거나 $2}}",
        "api-help-param-limit": "$1 초과는 허용되지 않습니다.",
        "api-help-param-limit2": "$1 초과는 허용되지 않습니다. (봇의 경우 $2)",
index 8f8a4c3..4ba4062 100644 (file)
        "apihelp-query+links-example-simple": "Holl de Lengks vun dä Sigg <kbd>Main Page</kbd>",
        "apihelp-query+linkshere-summary": "Fengk alle Sigge, di op de aanjejovve Sigge lengke.",
        "apihelp-query+linkshere-param-prop": "Wat för en Eijeschaffte holle:",
-       "apihelp-query+linkshere-paramvalue-prop-pageid": "Page ID of each page.",
-       "apihelp-query+linkshere-paramvalue-prop-title": "Title of each page.",
-       "apihelp-query+linkshere-paramvalue-prop-redirect": "Flag if the page is a redirect.",
        "apihelp-query+linkshere-param-namespace": "Donn blohß Sigge en heh dä Appachtemangs metnämme.",
        "apihelp-query+linkshere-param-limit": "Wi vill holle?",
        "apihelp-query+linkshere-example-simple": "Holl en Leß vun Sigge, di op de Sigg „<code lang=\"en\" xml:lang=\"en\" dir=\"ltr\">[[Main Page]]</code>“ lengke donn.",
index 9d43533..9b4ac5f 100644 (file)
        "apihelp-query+search-paramvalue-prop-hasrelated": "Zignorowano",
        "apihelp-query+search-param-limit": "Łączna liczba stron do zwrócenia.",
        "apihelp-query+search-param-interwiki": "Dołączaj wyniki wyszukiwań interwiki w wyszukiwarce, jeśli możliwe.",
+       "apihelp-query+search-param-sort": "Ustaw porządek sortowania zwracanych wyników.",
        "apihelp-query+search-example-simple": "Szukaj <kbd>meaning</kbd>.",
        "apihelp-query+siteinfo-paramvalue-prop-general": "Ogólne informacje o systemie.",
        "apihelp-query+siteinfo-paramvalue-prop-namespaces": "Lista zarejestrowanych przestrzeni nazw i ich nazwy kanoniczne.",
index 65c3cff..c177d2e 100644 (file)
        "apihelp-query+search-param-interwiki": "Inclua resultados de interwiki na pesquisa, se disponível.",
        "apihelp-query+search-param-backend": "Qual o backend de pesquisa a ser usado, se não for o padrão.",
        "apihelp-query+search-param-enablerewrites": "Habilita a reescrita de consulta interna. Alguns backends de pesquisa podem reescrever a consulta em outro que é pensado para fornecer melhores resultados, por exemplo, corrigindo erros de ortografia.",
+       "apihelp-query+search-param-sort": "Definir o ordenamento dos resultados devolvidos.",
        "apihelp-query+search-example-simple": "Procurar por <kbd>meaning</kbd>.",
        "apihelp-query+search-example-text": "Procurar textos para <kbd>meaning</kbd>.",
        "apihelp-query+search-example-generator": "Obter informações da página sobre as páginas retornadas para uma pesquisa por <kbd>meaning</kbd>.",
index 68eda63..3240c66 100644 (file)
        "apihelp-query+search-param-interwiki": "Incluir resultados interwikis na pesquisa, se disponíveis.",
        "apihelp-query+search-param-backend": "O servidor de pesquisas a ser usado, se diferente do servidor padrão.",
        "apihelp-query+search-param-enablerewrites": "Ativar a reescrita da pesquisa interna. Alguns motores de pesquisa podem reescrever a pesquisa substituindo-a por outra que consideram que dará melhores resultados, por exemplo, por corrigir erros de ortografia.",
+       "apihelp-query+search-param-sort": "Definir o ordenamento dos resultados devolvidos.",
        "apihelp-query+search-example-simple": "Pesquisar <kbd>meaning</kbd>.",
        "apihelp-query+search-example-text": "Pesquisar <kbd>meaning</kbd> nos textos.",
        "apihelp-query+search-example-generator": "Obter informação sobre as páginas devolvidas por uma pesquisa do termo <kbd>meaning</kbd>.",
index 3fcf499..1680bb8 100644 (file)
        "apihelp-query+search-param-interwiki": "{{doc-apihelp-param|query+search|interwiki}}",
        "apihelp-query+search-param-backend": "{{doc-apihelp-param|query+search|backend}}",
        "apihelp-query+search-param-enablerewrites": "{{doc-apihelp-param|query+search|enablerewrites}}",
+       "apihelp-query+search-param-sort": "{{doc-apihelp-param|query+search|sort}}",
        "apihelp-query+search-example-simple": "{{doc-apihelp-example|query+search}}",
        "apihelp-query+search-example-text": "{{doc-apihelp-example|query+search}}",
        "apihelp-query+search-example-generator": "{{doc-apihelp-example|query+search}}",
index 91aac43..e04f090 100644 (file)
        "apihelp-query+search-param-interwiki": "Включить результаты из других вики, если доступны.",
        "apihelp-query+search-param-backend": "Какой поисковый движок использовать, если не стандартный.",
        "apihelp-query+search-param-enablerewrites": "Разрешить редактирование запроса. Некоторые поисковые движки могут отредактировать запрос, например, исправив опечатку, если посчитают, что это приведёт к лучшим результатам.",
+       "apihelp-query+search-param-sort": "Задаёт порядок сортировки возвращаемых результатов.",
        "apihelp-query+search-example-simple": "Найти <kbd>meaning</kbd>.",
        "apihelp-query+search-example-text": "Найти тексты, содержащие <kbd>meaning</kbd>.",
        "apihelp-query+search-example-generator": "Получить информацию о страницах, возвращённых по поисковому запросу <kbd>meaning</kbd>.",
index 85d3982..818f717 100644 (file)
        "apihelp-query+search-param-interwiki": "搜索结果中包含跨wiki结果,如果可用。",
        "apihelp-query+search-param-backend": "要使用的搜索后端,如果没有则为默认。",
        "apihelp-query+search-param-enablerewrites": "启用内部查询重写。一些搜索后端可以重写查询到另一个被认为能提供更好结果的位置,例如纠正拼写错误。",
+       "apihelp-query+search-param-sort": "设置返回结果的排序。",
        "apihelp-query+search-example-simple": "搜索<kbd>meaning</kbd>。",
        "apihelp-query+search-example-text": "搜索文本<kbd>meaning</kbd>。",
        "apihelp-query+search-example-generator": "获取有关搜索<kbd>meaning</kbd>返回页面的页面信息。",
index 161dd56..3260ce4 100644 (file)
@@ -771,7 +771,12 @@ class AuthManager implements LoggerAwareInterface {
                        $status = self::SEC_FAIL;
                }
 
-               $this->logger->info( __METHOD__ . ": $operation is $status" );
+               $this->logger->info( __METHOD__ . ": $operation is $status for '{user}'",
+                       [
+                               'user' => $session->getUser()->getName(),
+                               'clientip' => $this->getRequest()->getIP(),
+                       ]
+               );
 
                return $status;
        }
index 2d08895..7cbb86f 100644 (file)
@@ -321,7 +321,7 @@ class LinkCache {
         */
        public function invalidateTitle( LinkTarget $title ) {
                if ( $this->isCacheable( $title ) ) {
-                       $cache = ObjectCache::getMainWANInstance();
+                       $cache = $this->wanCache;
                        $cache->delete(
                                $cache->makeKey( 'page', $title->getNamespace(), sha1( $title->getDBkey() ) )
                        );
index dd9e8e1..90108eb 100644 (file)
@@ -292,11 +292,7 @@ class LocalisationCache {
                        $this->loadSubitem( $code, $key, $subkey );
                }
 
-               if ( isset( $this->data[$code][$key][$subkey] ) ) {
-                       return $this->data[$code][$key][$subkey];
-               } else {
-                       return null;
-               }
+               return $this->data[$code][$key][$subkey] ?? null;
        }
 
        /**
@@ -603,11 +599,7 @@ class LocalisationCache {
                if ( $this->pluralRules === null ) {
                        $this->loadPluralFiles();
                }
-               if ( !isset( $this->pluralRules[$code] ) ) {
-                       return null;
-               } else {
-                       return $this->pluralRules[$code];
-               }
+               return $this->pluralRules[$code] ?? null;
        }
 
        /**
@@ -621,11 +613,7 @@ class LocalisationCache {
                if ( $this->pluralRuleTypes === null ) {
                        $this->loadPluralFiles();
                }
-               if ( !isset( $this->pluralRuleTypes[$code] ) ) {
-                       return null;
-               } else {
-                       return $this->pluralRuleTypes[$code];
-               }
+               return $this->pluralRuleTypes[$code] ?? null;
        }
 
        /**
@@ -1047,11 +1035,7 @@ class LocalisationCache {
                }
 
                foreach ( $data['preloadedMessages'] as $subkey ) {
-                       if ( isset( $data['messages'][$subkey] ) ) {
-                               $subitem = $data['messages'][$subkey];
-                       } else {
-                               $subitem = null;
-                       }
+                       $subitem = $data['messages'][$subkey] ?? null;
                        $preload['messages'][$subkey] = $subitem;
                }
 
index 60fe850..904090f 100644 (file)
@@ -520,11 +520,7 @@ class RecentChange {
                                continue;
                        }
 
-                       if ( isset( $this->mExtra['actionCommentIRC'] ) ) {
-                               $actionComment = $this->mExtra['actionCommentIRC'];
-                       } else {
-                               $actionComment = null;
-                       }
+                       $actionComment = $this->mExtra['actionCommentIRC'] ?? null;
 
                        $feed = RCFeed::factory( $params );
                        $feed->notify( $this, $actionComment );
@@ -662,9 +658,9 @@ class RecentChange {
         * Makes an entry in the database corresponding to an edit
         *
         * @param string $timestamp
-        * @param Title &$title
+        * @param Title $title
         * @param bool $minor
-        * @param User &$user
+        * @param User $user
         * @param string $comment
         * @param int $oldId
         * @param string $lastTimestamp
@@ -678,7 +674,7 @@ class RecentChange {
         * @return RecentChange
         */
        public static function notifyEdit(
-               $timestamp, &$title, $minor, &$user, $comment, $oldId, $lastTimestamp,
+               $timestamp, $title, $minor, $user, $comment, $oldId, $lastTimestamp,
                $bot, $ip = '', $oldSize = 0, $newSize = 0, $newId = 0, $patrol = 0,
                $tags = []
        ) {
@@ -739,9 +735,9 @@ class RecentChange {
         * Note: the title object must be loaded with the new id using resetArticleID()
         *
         * @param string $timestamp
-        * @param Title &$title
+        * @param Title $title
         * @param bool $minor
-        * @param User &$user
+        * @param User $user
         * @param string $comment
         * @param bool $bot
         * @param string $ip
@@ -752,7 +748,7 @@ class RecentChange {
         * @return RecentChange
         */
        public static function notifyNew(
-               $timestamp, &$title, $minor, &$user, $comment, $bot,
+               $timestamp, $title, $minor, $user, $comment, $bot,
                $ip = '', $size = 0, $newId = 0, $patrol = 0, $tags = []
        ) {
                $rc = new RecentChange;
@@ -809,8 +805,8 @@ class RecentChange {
 
        /**
         * @param string $timestamp
-        * @param Title &$title
-        * @param User &$user
+        * @param Title $title
+        * @param User $user
         * @param string $actionComment
         * @param string $ip
         * @param string $type
@@ -822,7 +818,7 @@ class RecentChange {
         * @param string $actionCommentIRC
         * @return bool
         */
-       public static function notifyLog( $timestamp, &$title, &$user, $actionComment, $ip, $type,
+       public static function notifyLog( $timestamp, $title, $user, $actionComment, $ip, $type,
                $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = ''
        ) {
                global $wgLogRestrictions;
@@ -840,8 +836,8 @@ class RecentChange {
 
        /**
         * @param string $timestamp
-        * @param Title &$title
-        * @param User &$user
+        * @param Title $title
+        * @param User $user
         * @param string $actionComment
         * @param string $ip
         * @param string $type
@@ -855,7 +851,7 @@ class RecentChange {
         * @param bool $isPatrollable Whether this log entry is patrollable
         * @return RecentChange
         */
-       public static function newLogEntry( $timestamp, &$title, &$user, $actionComment, $ip,
+       public static function newLogEntry( $timestamp, $title, $user, $actionComment, $ip,
                $type, $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = '',
                $revId = 0, $isPatrollable = false ) {
                global $wgRequest;
index d019f41..bd9cedc 100644 (file)
@@ -21,7 +21,9 @@
  * @ingroup Change tagging
  */
 
+use MediaWiki\Logger\LoggerFactory;
 use MediaWiki\MediaWikiServices;
+use MediaWiki\Storage\NameTableStore;
 use Wikimedia\Rdbms\Database;
 
 class ChangeTags {
@@ -346,31 +348,32 @@ class ChangeTags {
                if ( count( $tagsToAdd ) ) {
                        $changeTagMapping = [];
                        if ( $wgChangeTagsSchemaMigrationStage > MIGRATION_OLD ) {
-                               $tagDefRows = [];
+                               $changeTagDefStore = new NameTableStore(
+                                       MediaWikiServices::getInstance()->getDBLoadBalancer(),
+                                       MediaWikiServices::getInstance()->getMainWANObjectCache(),
+                                       LoggerFactory::getInstance( 'NameTableSqlStore' ),
+                                       'change_tag_def',
+                                       'ctd_id',
+                                       'ctd_name',
+                                       null,
+                                       false,
+                                       function ( $insertFields ) {
+                                               $insertFields['ctd_user_defined'] = 0;
+                                               $insertFields['ctd_count'] = 0;
+                                               return $insertFields;
+                                       }
+                               );
+
                                foreach ( $tagsToAdd as $tag ) {
-                                       $tagDefRows[] = [
-                                               'ctd_name' => $tag,
-                                               'ctd_user_defined' => 0,
-                                               'ctd_count' => 1
-                                       ];
+                                       $changeTagMapping[$tag] = $changeTagDefStore->acquireId( $tag );
                                }
 
-                               $dbw->upsert(
+                               $dbw->update(
                                        'change_tag_def',
-                                       $tagDefRows,
-                                       [ 'ctd_name' ],
                                        [ 'ctd_count = ctd_count + 1' ],
+                                       [ 'ctd_name' => $tagsToAdd ],
                                        __METHOD__
                                );
-
-                               $res = $dbw->select(
-                                       'change_tag_def',
-                                       [ 'ctd_name', 'ctd_id' ],
-                                       [ 'ctd_name' => $tagsToAdd ]
-                               );
-                               foreach ( $res as $row ) {
-                                       $changeTagMapping[$row->ctd_name] = $row->ctd_id;
-                               }
                        }
 
                        $tagsRows = [];
index d92c215..5f401a5 100644 (file)
@@ -577,10 +577,6 @@ class IcuCollation extends Collation {
                        '3.4' => '4.1',
                ];
 
-               if ( isset( $map[$versionPrefix] ) ) {
-                       return $map[$versionPrefix];
-               } else {
-                       return false;
-               }
+               return $map[$versionPrefix] ?? false;
        }
 }
index 3c86d11..9f6257c 100644 (file)
@@ -90,6 +90,7 @@ class LinksDeletionUpdate extends DataUpdate implements EnqueueableDataUpdate {
                foreach ( $catBatches as $catBatch ) {
                        $this->page->updateCategoryCounts( [], $catBatch, $id );
                        if ( count( $catBatches ) > 1 ) {
+                               // Only sacrifice atomicity if necessary due to size
                                $lbFactory->commitAndWaitForReplication(
                                        __METHOD__, $this->ticket, [ 'domain' => $dbw->getDomainID() ]
                                );
@@ -98,19 +99,10 @@ class LinksDeletionUpdate extends DataUpdate implements EnqueueableDataUpdate {
 
                // Refresh counts on categories that should be empty now
                if ( $title->getNamespace() === NS_CATEGORY ) {
-                       $row = $dbw->selectRow(
-                               'category',
-                               [ 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ],
-                               [ 'cat_title' => $title->getDBkey(), 'cat_pages <= 100' ],
-                               __METHOD__
-                       );
-                       if ( $row ) {
-                               $cat = Category::newFromRow( $row, $title );
-                               // T166757: do the update after the main job DB commit
-                               DeferredUpdates::addCallableUpdate( function () use ( $cat ) {
-                                       $cat->refreshCounts();
-                               } );
-                       }
+                       // T166757: do the update after the main job DB commit
+                       DeferredUpdates::addCallableUpdate( function () use ( $title ) {
+                               $this->refreshCategoryIfEmpty( $title );
+                       } );
                }
 
                $this->batchDeleteByPK(
@@ -195,6 +187,35 @@ class LinksDeletionUpdate extends DataUpdate implements EnqueueableDataUpdate {
                ScopedCallback::consume( $scopedLock );
        }
 
+       /**
+        * @param Title $title
+        */
+       private function refreshCategoryIfEmpty( Title $title ) {
+               $dbw = $this->getDB();
+
+               $row = $dbw->selectRow(
+                       'category',
+                       [ 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ],
+                       [ 'cat_title' => $title->getDBkey(), 'cat_pages <= 100' ],
+                       __METHOD__
+               );
+
+               if ( !$row ) {
+                       return; // nothing to delete
+               }
+
+               $cat = Category::newFromRow( $row, $title );
+               $hasLink = $dbw->selectField(
+                       'categorylinks',
+                       '1',
+                       [ 'cl_to' => $title->getDBkey() ],
+                       __METHOD__
+               );
+               if ( !$hasLink ) {
+                       $cat->refreshCounts(); // delete the category table entry
+               }
+       }
+
        private function batchDeleteByPK( $table, array $conds, array $pk, $bSize ) {
                $services = MediaWikiServices::getInstance();
                $lbFactory = $services->getDBLoadBalancerFactory();
index 910d221..7007316 100644 (file)
@@ -27,6 +27,8 @@ use ParserOutput;
 /**
  * Represents information returned by WikiPage::prepareContentForEdit()
  *
+ * @deprecated since 1.32, use DerivedPageDataUpdater instead.
+ *
  * @since 1.30
  */
 class PreparedEdit {
index 70068b9..3d9a904 100644 (file)
@@ -1177,11 +1177,7 @@ class FileRepo {
                if ( $status->successCount == 0 ) {
                        $status->setOK( false );
                }
-               if ( isset( $status->value[0] ) ) {
-                       $status->value = $status->value[0];
-               } else {
-                       $status->value = false;
-               }
+               $status->value = $status->value[0] ?? false;
 
                return $status;
        }
index 89287af..fa4567e 100644 (file)
@@ -324,11 +324,8 @@ class RepoGroup {
                }
                if ( $index === 'local' ) {
                        return $this->localRepo;
-               } elseif ( isset( $this->foreignRepos[$index] ) ) {
-                       return $this->foreignRepos[$index];
-               } else {
-                       return false;
                }
+               return $this->foreignRepos[$index] ?? false;
        }
 
        /**
index adad9ad..e86f292 100644 (file)
@@ -2208,7 +2208,7 @@ class LocalFile extends File {
 
                // If extra data (metadata) was not loaded then it must have been large
                return $this->extraDataLoaded
-               && strlen( serialize( $this->metadata ) ) <= self::CACHE_FIELD_MAX_LEN;
+                       && strlen( serialize( $this->metadata ) ) <= self::CACHE_FIELD_MAX_LEN;
        }
 
        /**
@@ -2216,9 +2216,9 @@ class LocalFile extends File {
         * @since 1.28
         */
        public function acquireFileLock() {
-               return $this->getRepo()->getBackend()->lockFiles(
+               return Status::wrap( $this->getRepo()->getBackend()->lockFiles(
                        [ $this->getPath() ], LockManager::LOCK_EX, 10
-               );
+               ) );
        }
 
        /**
@@ -2226,9 +2226,9 @@ class LocalFile extends File {
         * @since 1.28
         */
        public function releaseFileLock() {
-               return $this->getRepo()->getBackend()->unlockFiles(
+               return Status::wrap( $this->getRepo()->getBackend()->unlockFiles(
                        [ $this->getPath() ], LockManager::LOCK_EX
-               );
+               ) );
        }
 
        /**
index df44626..d885c9d 100644 (file)
@@ -239,11 +239,7 @@ class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable {
        }
 
        public function getDefault() {
-               if ( isset( $this->mDefault ) ) {
-                       return $this->mDefault;
-               } else {
-                       return [];
-               }
+               return $this->mDefault ?? [];
        }
 
        public function filterDataForSubmit( $data ) {
index f840b49..719478e 100644 (file)
@@ -5,7 +5,8 @@
                        "පසිඳු කාවින්ද",
                        "Subi",
                        "Sator",
-                       "Mikel Ibaiba"
+                       "Mikel Ibaiba",
+                       "Fitoschido"
                ]
        },
        "config-desc": "MediaWiki instalatzailea",
        "config-install-done-path": "<strong>Zorionak!</strong>\nMediaWiki instalatu duzu.\n\nInstalatzaileak sortu egin du <code>LocalSettings.php</code>\nZure konfigurazio guztia dauka.\n\nDeskargatu egin behar duzu eta jarri <code>$4</code> -ean . Deskarga automakikoki hasiko da.\n\nEz badizu deskargatzeko aukerarik eman, edo kantzalatu egin baduzu, hurrengo linkean klikatu berrabiatzeko:\n\n$3\n\n<strong>Oharra:</strong> Instalazio prozesuatik ateratzen bazara konfigurazio artxikoa deskargatu barik, gero ez da egongo eskuragarri.\n\nBehin hori eginda, <strong>[$2 enter your wiki]</strong> ahal duzu.",
        "config-install-success": "MediaWiki arrakastaz instalatu da. Orain <$1$2> bisitatu dezakezu zure wikia ikusteko.\nGalderarik izanez gero, begiratu gure maiztasunez egiten diren galderen zerrenda:\n<https://www.mediawiki.org/wiki/Manual:FAQ> edo erabili orrialde honi lotuta dauden laguntza foroetako bat.",
        "config-download-localsettings": "Jaitsi <code>LocalSettings.php</code>",
-       "config-help": "Laguntza",
+       "config-help": "laguntza",
        "config-help-tooltip": "sakatu zabaltzeko",
        "config-nofile": "Ezin da \"$1\" fitxategia aurkitu. Ezabatua izan da?",
        "config-extension-link": "Ba al zenekien wikiak [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions extensions] onartzen dituela?\n\nArakatu [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category extensions by category] edo [https://www.mediawiki.org/wiki/Extension_Matrix Extension Matrix] ikusi ahal izateko luzapenen zerrenda.",
index d9b6108..525ee6b 100644 (file)
        "config-nofile": "\"$1\" fájl nem található. Törölve lett?",
        "config-extension-link": "Tudtad, hogy a wikid támogat [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions kiterjesztéseket]?\n\nBöngészhetsz [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category kiterjesztéseket kategóriánként] vagy válogathatsz a [https://www.mediawiki.org/wiki/Extension_Matrix kiterjesztésmátrixból] az összes kiterjesztés áttekintéséhez.",
        "config-skins-screenshots": "$1 (képernyőképek: $2)",
+       "config-extensions-requires": "$1 ($2 szükséges hozzá)",
        "config-screenshot": "képernyőkép",
        "mainpagetext": "<strong>A MediaWiki telepítése sikeresen befejeződött.</strong>",
        "mainpagedocfooter": "Ha segítségre van szükséged a wikiszoftver használatához, akkor keresd fel a [https://meta.wikimedia.org/wiki/Help:Contents User's Guide] oldalt.\n\n== Alapok (angol nyelven) ==\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Configuration_settings Beállítások listája]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ MediaWiki GyIK]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-announce MediaWiki-kiadások levelezőlistája]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation#Translation_resources A MediaWiki fordítása a saját nyelvedre]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Combating_spam Tudd meg többet, hogyan küzdhetsz a kéretlen levelek ellen a wikiden]"
index cb60b01..20ddf72 100644 (file)
@@ -50,9 +50,9 @@ class MultiHttpClient implements LoggerAwareInterface {
        protected $multiHandle = null; // curl_multi handle
        /** @var string|null SSL certificates path */
        protected $caBundlePath;
-       /** @var int */
+       /** @var float */
        protected $connTimeout = 10;
-       /** @var int */
+       /** @var float */
        protected $reqTimeout = 300;
        /** @var bool */
        protected $usePipelining = false;
@@ -65,6 +65,11 @@ class MultiHttpClient implements LoggerAwareInterface {
        /** @var LoggerInterface */
        protected $logger;
 
+       // In PHP 7 due to https://bugs.php.net/bug.php?id=76480 the request/connect
+       // timeouts are periodically polled instead of being accurately respected.
+       // The select timeout is set to the minimum timeout multiplied by this factor.
+       const TIMEOUT_ACCURACY_FACTOR = 0.1;
+
        /**
         * @param array $options
         *   - connTimeout     : default connection timeout (seconds)
@@ -148,6 +153,8 @@ class MultiHttpClient implements LoggerAwareInterface {
        public function runMulti( array $reqs, array $opts = [] ) {
                $chm = $this->getCurlMulti();
 
+               $selectTimeout = $this->getSelectTimeout( $opts );
+
                // Normalize $reqs and add all of the required cURL handles...
                $handles = [];
                foreach ( $reqs as $index => &$req ) {
@@ -224,7 +231,7 @@ class MultiHttpClient implements LoggerAwareInterface {
                                } while ( $mrc == CURLM_CALL_MULTI_PERFORM );
                                // Wait (if possible) for available work...
                                if ( $active > 0 && $mrc == CURLM_OK ) {
-                                       if ( curl_multi_select( $chm, 10 ) == -1 ) {
+                                       if ( curl_multi_select( $chm, $selectTimeout ) == -1 ) {
                                                // PHP bug 63411; https://curl.haxx.se/libcurl/c/curl_multi_fdset.html
                                                usleep( 5000 ); // 5ms
                                        }
@@ -285,11 +292,11 @@ class MultiHttpClient implements LoggerAwareInterface {
        protected function getCurlHandle( array &$req, array $opts = [] ) {
                $ch = curl_init();
 
-               curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT,
-                       $opts['connTimeout'] ?? $this->connTimeout );
+               curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT_MS,
+                       ( $opts['connTimeout'] ?? $this->connTimeout ) * 1000 );
                curl_setopt( $ch, CURLOPT_PROXY, $req['proxy'] ?? $this->proxy );
-               curl_setopt( $ch, CURLOPT_TIMEOUT,
-                       $opts['reqTimeout'] ?? $this->reqTimeout );
+               curl_setopt( $ch, CURLOPT_TIMEOUT_MS,
+                       ( $opts['reqTimeout'] ?? $this->reqTimeout ) * 1000 );
                curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1 );
                curl_setopt( $ch, CURLOPT_MAXREDIRS, 4 );
                curl_setopt( $ch, CURLOPT_HEADER, 0 );
@@ -410,6 +417,28 @@ class MultiHttpClient implements LoggerAwareInterface {
                return $ch;
        }
 
+       /**
+        * Get a suitable select timeout for the given options.
+        *
+        * @param array $opts
+        * @return float
+        */
+       private function getSelectTimeout( $opts ) {
+               $connTimeout = $opts['connTimeout'] ?? $this->connTimeout;
+               $reqTimeout = $opts['reqTimeout'] ?? $this->reqTimeout;
+               $timeouts = array_filter( [ $connTimeout, $reqTimeout ] );
+               if ( count( $timeouts ) === 0 ) {
+                       return 1;
+               }
+
+               $selectTimeout = min( $timeouts ) * self::TIMEOUT_ACCURACY_FACTOR;
+               // Minimum 10us for sanity
+               if ( $selectTimeout < 10e-6 ) {
+                       $selectTimeout = 10e-6;
+               }
+               return $selectTimeout;
+       }
+
        /**
         * @return resource
         * @throws Exception
index 72ca590..b0dc10e 100644 (file)
@@ -44,7 +44,7 @@ class ChronologyProtector implements LoggerAwareInterface {
        /** @var string Hash of client parameters */
        protected $clientId;
        /** @var string[] Map of client information fields for logging */
-       protected $clientInfo;
+       protected $clientLogInfo;
        /** @var int|null Expected minimum index of the last write to the position store */
        protected $waitForPosIndex;
        /** @var int Max seconds to wait on positions to appear */
@@ -84,7 +84,11 @@ class ChronologyProtector implements LoggerAwareInterface {
                $this->key = $store->makeGlobalKey( __CLASS__, $this->clientId, 'v2' );
                $this->waitForPosIndex = $posIndex;
 
-               $this->clientInfo = $client + [ 'clientId' => '' ];
+               $this->clientLogInfo = [
+                       'clientIP' => $client['ip'],
+                       'clientAgent' => $client['agent'],
+                       'clientId' => $client['clientId'] ?? null
+               ];
 
                $this->logger = new NullLogger();
        }
@@ -313,7 +317,7 @@ class ChronologyProtector implements LoggerAwareInterface {
                                                [
                                                        'cpPosIndex' => $this->waitForPosIndex,
                                                        'waitTimeMs' => $waitedMs
-                                               ] + $this->clientInfo
+                                               ] + $this->clientLogInfo
                                        );
                                } else {
                                        $this->logger->warning(
@@ -322,7 +326,7 @@ class ChronologyProtector implements LoggerAwareInterface {
                                                        'cpPosIndex' => $this->waitForPosIndex,
                                                        'indexReached' => $indexReached,
                                                        'waitTimeMs' => $waitedMs
-                                               ] + $this->clientInfo
+                                               ] + $this->clientLogInfo
                                        );
                                }
                        } else {
index 78c6d91..bbc656d 100644 (file)
@@ -372,7 +372,7 @@ abstract class DatabaseMysqlBase extends Database {
 
                // Unfortunately, mysql_num_rows does not reset the last errno.
                // We are not checking for any errors here, since
-               // these are no errors mysql_num_rows can cause.
+               // there are no errors mysql_num_rows can cause.
                // See https://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
                // See https://phabricator.wikimedia.org/T44430
                return $n;
index b226f0e..9a30383 100644 (file)
@@ -509,6 +509,10 @@ abstract class LBFactory implements ILBFactory {
                        // Request opted out of using position wait logic. This is useful for requests
                        // done by the job queue or background ETL that do not have a meaningful session.
                        $this->chronProt->setWaitEnabled( false );
+               } elseif ( $this->memStash instanceof EmptyBagOStuff ) {
+                       // No where to store any DB positions and wait for them to appear
+                       $this->chronProt->setEnabled( false );
+                       $this->replLogger->info( 'Cannot use ChronologyProtector with EmptyBagOStuff.' );
                }
 
                $this->replLogger->debug( __METHOD__ . ': using request info ' .
@@ -655,14 +659,14 @@ abstract class LBFactory implements ILBFactory {
 
        /**
         * @param string $value Possible result of LBFactory::makeCookieValueFromCPIndex()
-        * @param int $minTimestamp Lowest UNIX timestamp of non-expired values (if present)
+        * @param int $minTimestamp Lowest UNIX timestamp that a non-expired value can have
         * @return array (index: int or null, clientId: string or null)
         * @since 1.32
         */
        public static function getCPInfoFromCookieValue( $value, $minTimestamp ) {
                static $placeholder = [ 'index' => null, 'clientId' => null ];
 
-               if ( !preg_match( '/^(\d+)(?:@(\d+))?(?:#([0-9a-f]{32}))?$/', $value, $m ) ) {
+               if ( !preg_match( '/^(\d+)@(\d+)#([0-9a-f]{32})$/', $value, $m ) ) {
                        return $placeholder; // invalid
                }
 
index 06915b2..5ef0135 100644 (file)
@@ -92,10 +92,11 @@ class BufferingStatsdDataFactory extends StatsdDataFactory implements IBuffering
        }
 
        /**
-        * @deprecated Use getData()
+        * @deprecated since 1.30 Use getData() instead
         * @return StatsdData[]
         */
        public function getBuffer() {
+               wfDeprecated( __METHOD__, '1.30' );
                return $this->buffer;
        }
 
@@ -103,6 +104,10 @@ class BufferingStatsdDataFactory extends StatsdDataFactory implements IBuffering
                return !empty( $this->buffer );
        }
 
+       /**
+        * @since 1.30
+        * @return StatsdData[]
+        */
        public function getData() {
                return $this->buffer;
        }
index 87d7e0a..d096b00 100644 (file)
@@ -204,7 +204,7 @@ class LinkRenderer {
                        $realHtml = $html = null;
                }
                if ( !Hooks::run( 'LinkBegin',
-                       [ $dummy, $title, &$html, &$extraAttribs, &$query, &$options, &$ret ] )
+                       [ $dummy, $title, &$html, &$extraAttribs, &$query, &$options, &$ret ], '1.28' )
                ) {
                        return $ret;
                }
@@ -373,7 +373,7 @@ class LinkRenderer {
                        $title = Title::newFromLinkTarget( $target );
                        $options = $this->getLegacyOptions( $isKnown );
                        if ( !Hooks::run( 'LinkEnd',
-                               [ $dummy, $title, $options, &$html, &$attribs, &$ret ] )
+                               [ $dummy, $title, $options, &$html, &$attribs, &$ret ], '1.28' )
                        ) {
                                return $ret;
                        }
index 52d7373..9ebc63f 100644 (file)
@@ -1859,9 +1859,9 @@ class FormatMetadata extends ContextSource {
                // drop all characters which are not valid in an XML tag name
                // a bunch of non-ASCII letters would be valid but probably won't
                // be used so we take the easy way
-               $key = preg_replace( '/[^a-zA-z0-9_:.-]/', '', $key );
+               $key = preg_replace( '/[^a-zA-z0-9_:.\-]/', '', $key );
                // drop characters which are invalid at the first position
-               $key = preg_replace( '/^[\d-.]+/', '', $key );
+               $key = preg_replace( '/^[\d\-.]+/', '', $key );
 
                if ( $key == '' ) {
                        $key = '_';
index e186279..5bbdb6c 100644 (file)
 use MediaWiki\Edit\PreparedEdit;
 use MediaWiki\Logger\LoggerFactory;
 use MediaWiki\MediaWikiServices;
+use MediaWiki\Storage\DerivedPageDataUpdater;
+use MediaWiki\Storage\PageUpdater;
+use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Storage\RevisionSlotsUpdate;
+use MediaWiki\Storage\RevisionStore;
 use Wikimedia\Assert\Assert;
 use Wikimedia\Rdbms\FakeResultWrapper;
 use Wikimedia\Rdbms\IDatabase;
-use Wikimedia\Rdbms\DBUnexpectedError;
+use Wikimedia\Rdbms\LoadBalancer;
 
 /**
  * Class representing a MediaWiki article and history.
@@ -88,6 +93,11 @@ class WikiPage implements Page, IDBAccessObject {
         */
        protected $mLinksUpdated = '19700101000000';
 
+       /**
+        * @var DerivedPageDataUpdater|null
+        */
+       private $derivedDataUpdater = null;
+
        /**
         * Constructor and clear the article
         * @param Title $title Reference to a Title object.
@@ -206,6 +216,27 @@ class WikiPage implements Page, IDBAccessObject {
                }
        }
 
+       /**
+        * @return RevisionStore
+        */
+       private function getRevisionStore() {
+               return MediaWikiServices::getInstance()->getRevisionStore();
+       }
+
+       /**
+        * @return ParserCache
+        */
+       private function getParserCache() {
+               return MediaWikiServices::getInstance()->getParserCache();
+       }
+
+       /**
+        * @return LoadBalancer
+        */
+       private function getDBLoadBalancer() {
+               return MediaWikiServices::getInstance()->getDBLoadBalancer();
+       }
+
        /**
         * @todo Move this UI stuff somewhere else
         *
@@ -261,8 +292,8 @@ class WikiPage implements Page, IDBAccessObject {
                $this->mTimestamp = '';
                $this->mIsRedirect = false;
                $this->mLatest = false;
-               // T59026: do not clear mPreparedEdit since prepareTextForEdit() already checks
-               // the requested rev ID and content against the cached one for equality. For most
+               // T59026: do not clear $this->derivedDataUpdater since getDerivedDataUpdater() already
+               // checks the requested rev ID and content against the cached one. For most
                // content types, the output should not change during the lifetime of this cache.
                // Clearing it can cause extra parses on edit for no reason.
        }
@@ -433,7 +464,7 @@ class WikiPage implements Page, IDBAccessObject {
 
                if ( is_int( $from ) ) {
                        list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from );
-                       $loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer();
+                       $loadBalancer = $this->getDBLoadBalancer();
                        $db = $loadBalancer->getConnection( $index );
                        $data = $this->pageDataFromTitle( $db, $this->mTitle, $opts );
 
@@ -456,6 +487,34 @@ class WikiPage implements Page, IDBAccessObject {
                $this->loadFromRow( $data, $from );
        }
 
+       /**
+        * Checks whether the page data was loaded using the given database access mode (or better).
+        *
+        * @since 1.32
+        *
+        * @param string|int $from One of the following:
+        *   - "fromdb" or WikiPage::READ_NORMAL to get from a replica DB.
+        *   - "fromdbmaster" or WikiPage::READ_LATEST to get from the master DB.
+        *   - "forupdate"  or WikiPage::READ_LOCKING to get from the master DB
+        *     using SELECT FOR UPDATE.
+        *
+        * @return bool
+        */
+       public function wasLoadedFrom( $from ) {
+               $from = self::convertSelectType( $from );
+
+               if ( !is_int( $from ) ) {
+                       // No idea from where the caller got this data, assume replica DB.
+                       $from = self::READ_NORMAL;
+               }
+
+               if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
+                       return true;
+               }
+
+               return false;
+       }
+
        /**
         * Load the object from a database row
         *
@@ -468,7 +527,7 @@ class WikiPage implements Page, IDBAccessObject {
         *          the master DB using SELECT FOR UPDATE
         */
        public function loadFromRow( $data, $from ) {
-               $lc = LinkCache::singleton();
+               $lc = MediaWikiServices::getInstance()->getLinkCache();
                $lc->clearLink( $this->mTitle );
 
                if ( $data ) {
@@ -843,11 +902,14 @@ class WikiPage implements Page, IDBAccessObject {
        public function isCountable( $editInfo = false ) {
                global $wgArticleCountMethod;
 
+               // NOTE: Keep in sync with DerivedPageDataUpdater::isCountable.
+
                if ( !$this->mTitle->isContentPage() ) {
                        return false;
                }
 
                if ( $editInfo ) {
+                       // NOTE: only the main slot can make a page a redirect
                        $content = $editInfo->pstContent;
                } else {
                        $content = $this->getContent();
@@ -1031,7 +1093,7 @@ class WikiPage implements Page, IDBAccessObject {
         * @return UserArrayFromResult
         */
        public function getContributors() {
-               // @todo FIXME: This is expensive; cache this info somewhere.
+               // @todo: This is expensive; cache this info somewhere.
 
                $dbr = wfGetDB( DB_REPLICA );
 
@@ -1119,7 +1181,7 @@ class WikiPage implements Page, IDBAccessObject {
                }
 
                if ( $useParserCache ) {
-                       $parserOutput = MediaWikiServices::getInstance()->getParserCache()
+                       $parserOutput = $this->getParserCache()
                                ->get( $this, $parserOptions );
                        if ( $parserOutput !== false ) {
                                return $parserOutput;
@@ -1197,6 +1259,8 @@ class WikiPage implements Page, IDBAccessObject {
         * or else the record will be left in a funky state.
         * Best if all done inside a transaction.
         *
+        * @todo Factor out into a PageStore service, to be used by PageUpdater.
+        *
         * @param IDatabase $dbw
         * @param int|null $pageId Custom page ID that will be used for the insert statement
         *
@@ -1237,6 +1301,8 @@ class WikiPage implements Page, IDBAccessObject {
        /**
         * Update the page record to point to a newly saved revision.
         *
+        * @todo Factor out into a PageStore service, or move into PageUpdater.
+        *
         * @param IDatabase $dbw
         * @param Revision $revision For ID number, and text used to set
         *   length and redirect status fields
@@ -1252,6 +1318,10 @@ class WikiPage implements Page, IDBAccessObject {
        ) {
                global $wgContentHandlerUseDB;
 
+               // TODO: move into PageUpdater or PageStore
+               // NOTE: when doing that, make sure cached fields get reset in doEditContent,
+               // and in the compat stub!
+
                // Assertion to try to catch T92046
                if ( (int)$revision->getId() === 0 ) {
                        throw new InvalidArgumentException(
@@ -1297,7 +1367,8 @@ class WikiPage implements Page, IDBAccessObject {
                        $this->mLatest = $revision->getId();
                        $this->mIsRedirect = (bool)$rt;
                        // Update the LinkCache.
-                       LinkCache::singleton()->addGoodLinkObj(
+                       $linkCache = MediaWikiServices::getInstance()->getLinkCache();
+                       $linkCache->addGoodLinkObj(
                                $this->getId(),
                                $this->mTitle,
                                $len,
@@ -1430,7 +1501,7 @@ class WikiPage implements Page, IDBAccessObject {
        ) {
                $baseRevId = null;
                if ( $edittime && $sectionId !== 'new' ) {
-                       $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
+                       $lb = $this->getDBLoadBalancer();
                        $dbr = $lb->getConnection( DB_REPLICA );
                        $rev = Revision::loadFromTimestamp( $dbr, $this->mTitle, $edittime );
                        // Try the master if this thread may have just added it.
@@ -1503,6 +1574,10 @@ class WikiPage implements Page, IDBAccessObject {
 
        /**
         * Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
+        *
+        * @deprecated since 1.32, use exists() instead, or simply omit the EDIT_UPDATE
+        * and EDIT_NEW flags. To protect against race conditions, use PageUpdater::grabParentRevision.
+        *
         * @param int $flags
         * @return int Updated $flags
         */
@@ -1518,12 +1593,132 @@ class WikiPage implements Page, IDBAccessObject {
                return $flags;
        }
 
+       /**
+        * @return DerivedPageDataUpdater
+        */
+       private function newDerivedDataUpdater() {
+               global $wgContLang, $wgRCWatchCategoryMembership, $wgArticleCountMethod;
+
+               $derivedDataUpdater = new DerivedPageDataUpdater(
+                       $this, // NOTE: eventually, PageUpdater should not know about WikiPage
+                       $this->getRevisionStore(),
+                       $this->getParserCache(),
+                       JobQueueGroup::singleton(),
+                       MessageCache::singleton(),
+                       $wgContLang,
+                       LoggerFactory::getInstance( 'SaveParse' )
+               );
+
+               $derivedDataUpdater->setRcWatchCategoryMembership( $wgRCWatchCategoryMembership );
+               $derivedDataUpdater->setArticleCountMethod( $wgArticleCountMethod );
+
+               return $derivedDataUpdater;
+       }
+
+       /**
+        * Returns a DerivedPageDataUpdater for use with the given target revision or new content.
+        * This method attempts to re-use the same DerivedPageDataUpdater instance for subsequent calls.
+        * The parameters passed to this method are used to ensure that the DerivedPageDataUpdater
+        * returned matches that caller's expectations, allowing an existing instance to be re-used
+        * if the given parameters match that instance's internal state according to
+        * DerivedPageDataUpdater::isReusableFor(), and creating a new instance of the parameters do not
+        * match the existign one.
+        *
+        * If neither $forRevision nor $forUpdate is given, a new DerivedPageDataUpdater is always
+        * created, replacing any DerivedPageDataUpdater currently cached.
+        *
+        * MCR migration note: this replaces WikiPage::prepareContentForEdit.
+        *
+        * @since 1.32
+        *
+        * @param User|null $forUser The user that will be used for, or was used for, PST.
+        * @param RevisionRecord|null $forRevision The revision created by the edit for which
+        *        to perform updates, if the edit was already saved.
+        * @param RevisionSlotsUpdate|null $forUpdate The new content to be saved by the edit (pre PST),
+        *        if the edit was not yet saved.
+        *
+        * @return DerivedPageDataUpdater
+        */
+       private function getDerivedDataUpdater(
+               User $forUser = null,
+               RevisionRecord $forRevision = null,
+               RevisionSlotsUpdate $forUpdate = null
+       ) {
+               if ( !$forRevision && !$forUpdate ) {
+                       // NOTE: can't re-use an existing derivedDataUpdater if we don't know what the caller is
+                       // going to use it with.
+                       $this->derivedDataUpdater = null;
+               }
+
+               if ( $this->derivedDataUpdater && !$this->derivedDataUpdater->isContentPrepared() ) {
+                       // NOTE: can't re-use an existing derivedDataUpdater if other code that has a reference
+                       // to it did not yet initialize it, because we don't know what data it will be
+                       // initialized with.
+                       $this->derivedDataUpdater = null;
+               }
+
+               // XXX: It would be nice to have an LRU cache instead of trying to re-use a single instance.
+               // However, there is no good way to construct a cache key. We'd need to check against all
+               // cached instances.
+
+               if ( $this->derivedDataUpdater
+                       && !$this->derivedDataUpdater->isReusableFor(
+                               $forUser,
+                               $forRevision,
+                               $forUpdate
+                       )
+               ) {
+                       $this->derivedDataUpdater = null;
+               }
+
+               if ( !$this->derivedDataUpdater ) {
+                       $this->derivedDataUpdater = $this->newDerivedDataUpdater();
+               }
+
+               return $this->derivedDataUpdater;
+       }
+
+       /**
+        * Returns a PageUpdater for creating new revisions on this page (or creating the page).
+        *
+        * The PageUpdater can also be used to detect the need for edit conflict resolution,
+        * and to protected such conflict resolution from concurrent edits using a check-and-set
+        * mechanism.
+        *
+        * @since 1.32
+        *
+        * @param User $user
+        *
+        * @return PageUpdater
+        */
+       public function newPageUpdater( User $user ) {
+               global $wgAjaxEditStash, $wgUseAutomaticEditSummaries, $wgPageCreationLog;
+
+               $pageUpdater = new PageUpdater(
+                       $user,
+                       $this, // NOTE: eventually, PageUpdater should not know about WikiPage
+                       $this->getDerivedDataUpdater( $user ),
+                       $this->getDBLoadBalancer(),
+                       $this->getRevisionStore()
+               );
+
+               $pageUpdater->setUsePageCreationLog( $wgPageCreationLog );
+               $pageUpdater->setAjaxEditStash( $wgAjaxEditStash );
+               $pageUpdater->setUseAutomaticEditSummaries( $wgUseAutomaticEditSummaries );
+
+               return $pageUpdater;
+       }
+
        /**
         * Change an existing article or create a new article. Updates RC and all necessary caches,
         * optionally via the deferred update array.
         *
+        * @deprecated since 1.32, use PageUpdater::saveRevision instead. Note that the new method
+        * expects callers to take care of checking EDIT_MINOR against the minoredit right, and to
+        * apply the autopatrol right as appropriate.
+        *
         * @param Content $content New content
-        * @param string $summary Edit summary
+        * @param string|CommentStoreComment $summary Edit summary
         * @param int $flags Bitfield:
         *      EDIT_NEW
         *          Article is known or assumed to be non-existent, create a new one
@@ -1551,8 +1746,7 @@ class WikiPage implements Page, IDBAccessObject {
         *   This is not the parent revision ID, rather the revision ID for older
         *   content used as the source for a rollback, for example.
         * @param User $user The user doing the edit
-        * @param string $serialFormat Format for storing the content in the
-        *   database.
+        * @param string $serialFormat IGNORED.
         * @param array|null $tags Change tags to apply to this edit
         * Callers are responsible for permission checks
         * (with ChangeTags::canAddTagsAccompanyingChange)
@@ -1580,422 +1774,58 @@ class WikiPage implements Page, IDBAccessObject {
                Content $content, $summary, $flags = 0, $baseRevId = false,
                User $user = null, $serialFormat = null, $tags = [], $undidRevId = 0
        ) {
-               global $wgUser, $wgUseAutomaticEditSummaries;
+               global $wgUser, $wgUseNPPatrol, $wgUseRCPatrol;
 
-               // Old default parameter for $tags was null
-               if ( $tags === null ) {
-                       $tags = [];
+               if ( !( $summary instanceof CommentStoreComment ) ) {
+                       $summary = CommentStoreComment::newUnsavedComment( trim( $summary ) );
                }
 
-               // Low-level sanity check
-               if ( $this->mTitle->getText() === '' ) {
-                       throw new MWException( 'Something is trying to edit an article with an empty title' );
-               }
-               // Make sure the given content type is allowed for this page
-               if ( !$content->getContentHandler()->canBeUsedOn( $this->mTitle ) ) {
-                       return Status::newFatal( 'content-not-allowed-here',
-                               ContentHandler::getLocalizedName( $content->getModel() ),
-                               $this->mTitle->getPrefixedText()
-                       );
-               }
-
-               // Load the data from the master database if needed.
-               // The caller may already loaded it from the master or even loaded it using
-               // SELECT FOR UPDATE, so do not override that using clear().
-               $this->loadPageData( 'fromdbmaster' );
-
-               $user = $user ?: $wgUser;
-               $flags = $this->checkFlags( $flags );
-
-               // Avoid PHP 7.1 warning of passing $this by reference
-               $wikiPage = $this;
-
-               // Trigger pre-save hook (using provided edit summary)
-               $hookStatus = Status::newGood( [] );
-               $hook_args = [ &$wikiPage, &$user, &$content, &$summary,
-                                                       $flags & EDIT_MINOR, null, null, &$flags, &$hookStatus ];
-               // Check if the hook rejected the attempted save
-               if ( !Hooks::run( 'PageContentSave', $hook_args ) ) {
-                       if ( $hookStatus->isOK() ) {
-                               // Hook returned false but didn't call fatal(); use generic message
-                               $hookStatus->fatal( 'edit-hook-aborted' );
-                       }
-
-                       return $hookStatus;
-               }
-
-               $old_revision = $this->getRevision(); // current revision
-               $old_content = $this->getContent( Revision::RAW ); // current revision's content
-
-               $handler = $content->getContentHandler();
-               $tag = $handler->getChangeTag( $old_content, $content, $flags );
-               // If there is no applicable tag, null is returned, so we need to check
-               if ( $tag ) {
-                       $tags[] = $tag;
-               }
-
-               // Check for undo tag
-               if ( $undidRevId !== 0 && in_array( 'mw-undo', ChangeTags::getSoftwareTags() ) ) {
-                       $tags[] = 'mw-undo';
-               }
-
-               // Provide autosummaries if summary is not provided and autosummaries are enabled
-               if ( $wgUseAutomaticEditSummaries && ( $flags & EDIT_AUTOSUMMARY ) && $summary == '' ) {
-                       $summary = $handler->getAutosummary( $old_content, $content, $flags );
-               }
-
-               // Avoid statsd noise and wasted cycles check the edit stash (T136678)
-               if ( ( $flags & EDIT_INTERNAL ) || ( $flags & EDIT_FORCE_BOT ) ) {
-                       $useCache = false;
-               } else {
-                       $useCache = true;
-               }
-
-               // Get the pre-save transform content and final parser output
-               $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialFormat, $useCache );
-               $pstContent = $editInfo->pstContent; // Content object
-               $meta = [
-                       'bot' => ( $flags & EDIT_FORCE_BOT ),
-                       'minor' => ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' ),
-                       'serialized' => $pstContent->serialize( $serialFormat ),
-                       'serialFormat' => $serialFormat,
-                       'baseRevId' => $baseRevId,
-                       'oldRevision' => $old_revision,
-                       'oldContent' => $old_content,
-                       'oldId' => $this->getLatest(),
-                       'oldIsRedirect' => $this->isRedirect(),
-                       'oldCountable' => $this->isCountable(),
-                       'tags' => ( $tags !== null ) ? (array)$tags : [],
-                       'undidRevId' => $undidRevId
-               ];
-
-               // Actually create the revision and create/update the page
-               if ( $flags & EDIT_UPDATE ) {
-                       $status = $this->doModify( $pstContent, $flags, $user, $summary, $meta );
-               } else {
-                       $status = $this->doCreate( $pstContent, $flags, $user, $summary, $meta );
+               if ( !$user ) {
+                       $user = $wgUser;
                }
 
-               // Promote user to any groups they meet the criteria for
-               DeferredUpdates::addCallableUpdate( function () use ( $user ) {
-                       $user->addAutopromoteOnceGroups( 'onEdit' );
-                       $user->addAutopromoteOnceGroups( 'onView' ); // b/c
-               } );
-
-               return $status;
-       }
-
-       /**
-        * @param Content $content Pre-save transform content
-        * @param int $flags
-        * @param User $user
-        * @param string $summary
-        * @param array $meta
-        * @return Status
-        * @throws DBUnexpectedError
-        * @throws Exception
-        * @throws FatalError
-        * @throws MWException
-        */
-       private function doModify(
-               Content $content, $flags, User $user, $summary, array $meta
-       ) {
-               global $wgUseRCPatrol;
-
-               // Update article, but only if changed.
-               $status = Status::newGood( [ 'new' => false, 'revision' => null ] );
-
-               // Convenience variables
-               $now = wfTimestampNow();
-               $oldid = $meta['oldId'];
-               /** @var Content|null $oldContent */
-               $oldContent = $meta['oldContent'];
-               $newsize = $content->getSize();
-
-               if ( !$oldid ) {
-                       // Article gone missing
-                       $status->fatal( 'edit-gone-missing' );
-
-                       return $status;
-               } elseif ( !$oldContent ) {
-                       // Sanity check for T39225
-                       throw new MWException( "Could not find text for current revision {$oldid}." );
+               // TODO: this check is here for backwards-compatibility with 1.31 behavior.
+               // Checking the minoredit right should be done in the same place the 'bot' right is
+               // checked for the EDIT_FORCE_BOT flag, which is currently in EditPage::attemptSave.
+               if ( ( $flags & EDIT_MINOR ) && !$user->isAllowed( 'minoredit' ) ) {
+                       $flags = ( $flags & ~EDIT_MINOR );
                }
 
-               $changed = !$content->equals( $oldContent );
-
-               $dbw = wfGetDB( DB_MASTER );
+               // NOTE: while doEditContent() executes, callbacks to getDerivedDataUpdater and
+               // prepareContentForEdit will generally use the DerivedPageDataUpdater that is also
+               // used by this PageUpdater. However, there is no guarantee for this.
+               $updater = $this->newPageUpdater( $user );
+               $updater->setContent( 'main', $content );
+               $updater->setBaseRevisionId( $baseRevId );
+               $updater->setUndidRevisionId( $undidRevId );
 
-               if ( $changed ) {
-                       // @TODO: pass content object?!
-                       $revision = new Revision( [
-                               'page'       => $this->getId(),
-                               'title'      => $this->mTitle, // for determining the default content model
-                               'comment'    => $summary,
-                               'minor_edit' => $meta['minor'],
-                               'text'       => $meta['serialized'],
-                               'len'        => $newsize,
-                               'parent_id'  => $oldid,
-                               'user'       => $user->getId(),
-                               'user_text'  => $user->getName(),
-                               'timestamp'  => $now,
-                               'content_model' => $content->getModel(),
-                               'content_format' => $meta['serialFormat'],
-                       ] );
+               $needsPatrol = $wgUseRCPatrol || ( $wgUseNPPatrol && !$this->exists() );
 
-                       $prepStatus = $content->prepareSave( $this, $flags, $oldid, $user );
-                       $status->merge( $prepStatus );
-                       if ( !$status->isOK() ) {
-                               return $status;
-                       }
-
-                       $dbw->startAtomic( __METHOD__ );
-                       // Get the latest page_latest value while locking it.
-                       // Do a CAS style check to see if it's the same as when this method
-                       // started. If it changed then bail out before touching the DB.
-                       $latestNow = $this->lockAndGetLatest();
-                       if ( $latestNow != $oldid ) {
-                               $dbw->endAtomic( __METHOD__ );
-                               // Page updated or deleted in the mean time
-                               $status->fatal( 'edit-conflict' );
-
-                               return $status;
-                       }
-
-                       // At this point we are now comitted to returning an OK
-                       // status unless some DB query error or other exception comes up.
-                       // This way callers don't have to call rollback() if $status is bad
-                       // unless they actually try to catch exceptions (which is rare).
-
-                       // Save the revision text
-                       $revisionId = $revision->insertOn( $dbw );
-                       // Update page_latest and friends to reflect the new revision
-                       if ( !$this->updateRevisionOn( $dbw, $revision, null, $meta['oldIsRedirect'] ) ) {
-                               throw new MWException( "Failed to update page row to use new revision." );
-                       }
-
-                       $tags = $meta['tags'];
-                       Hooks::run( 'NewRevisionFromEditComplete',
-                               [ $this, $revision, $meta['baseRevId'], $user, &$tags ] );
-
-                       // Update recentchanges
-                       if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
-                               // Mark as patrolled if the user can do so
-                               $autopatrolled = $wgUseRCPatrol && !count(
-                                               $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
-                               // Add RC row to the DB
-                               RecentChange::notifyEdit(
-                                       $now,
-                                       $this->mTitle,
-                                       $revision->isMinor(),
-                                       $user,
-                                       $summary,
-                                       $oldid,
-                                       $this->getTimestamp(),
-                                       $meta['bot'],
-                                       '',
-                                       $oldContent ? $oldContent->getSize() : 0,
-                                       $newsize,
-                                       $revisionId,
-                                       $autopatrolled ? RecentChange::PRC_AUTOPATROLLED :
-                                               RecentChange::PRC_UNPATROLLED,
-                                       $tags
-                               );
-                       }
-
-                       $user->incEditCount();
-
-                       $dbw->endAtomic( __METHOD__ );
-                       $this->mTimestamp = $now;
-               } else {
-                       // T34948: revision ID must be set to page {{REVISIONID}} and
-                       // related variables correctly. Likewise for {{REVISIONUSER}} (T135261).
-                       // Since we don't insert a new revision into the database, the least
-                       // error-prone way is to reuse given old revision.
-                       $revision = $meta['oldRevision'];
+               // TODO: this logic should not be in the storage layer, it's here for compatibility
+               // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
+               // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
+               if ( $needsPatrol && $this->getTitle()->userCan( 'autopatrol', $user ) ) {
+                       $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
                }
 
-               if ( $changed ) {
-                       // Return the new revision to the caller
-                       $status->value['revision'] = $revision;
-               } else {
-                       $status->warning( 'edit-no-change' );
-                       // Update page_touched as updateRevisionOn() was not called.
-                       // Other cache updates are managed in onArticleEdit() via doEditUpdates().
-                       $this->mTitle->invalidateCache( $now );
-               }
+               $updater->addTags( $tags );
 
-               // Do secondary updates once the main changes have been committed...
-               DeferredUpdates::addUpdate(
-                       new AtomicSectionUpdate(
-                               $dbw,
-                               __METHOD__,
-                               function () use (
-                                       $revision, &$user, $content, $summary, &$flags,
-                                       $changed, $meta, &$status
-                               ) {
-                                       // Update links tables, site stats, etc.
-                                       $this->doEditUpdates(
-                                               $revision,
-                                               $user,
-                                               [
-                                                       'changed' => $changed,
-                                                       'oldcountable' => $meta['oldCountable'],
-                                                       'oldrevision' => $meta['oldRevision']
-                                               ]
-                                       );
-                                       // Avoid PHP 7.1 warning of passing $this by reference
-                                       $wikiPage = $this;
-                                       // Trigger post-save hook
-                                       $params = [ &$wikiPage, &$user, $content, $summary, $flags & EDIT_MINOR,
-                                               null, null, &$flags, $revision, &$status, $meta['baseRevId'],
-                                               $meta['undidRevId'] ];
-                                       Hooks::run( 'PageContentSaveComplete', $params );
-                               }
-                       ),
-                       DeferredUpdates::PRESEND
+               $revRec = $updater->saveRevision(
+                       $summary,
+                       $flags
                );
 
-               return $status;
-       }
-
-       /**
-        * @param Content $content Pre-save transform content
-        * @param int $flags
-        * @param User $user
-        * @param string $summary
-        * @param array $meta
-        * @return Status
-        * @throws DBUnexpectedError
-        * @throws Exception
-        * @throws FatalError
-        * @throws MWException
-        */
-       private function doCreate(
-               Content $content, $flags, User $user, $summary, array $meta
-       ) {
-               global $wgUseRCPatrol, $wgUseNPPatrol, $wgPageCreationLog;
-
-               $status = Status::newGood( [ 'new' => true, 'revision' => null ] );
-
-               $now = wfTimestampNow();
-               $newsize = $content->getSize();
-               $prepStatus = $content->prepareSave( $this, $flags, $meta['oldId'], $user );
-               $status->merge( $prepStatus );
-               if ( !$status->isOK() ) {
-                       return $status;
-               }
-
-               $dbw = wfGetDB( DB_MASTER );
-               $dbw->startAtomic( __METHOD__ );
-
-               // Add the page record unless one already exists for the title
-               $newid = $this->insertOn( $dbw );
-               if ( $newid === false ) {
-                       $dbw->endAtomic( __METHOD__ ); // nothing inserted
-                       $status->fatal( 'edit-already-exists' );
-
-                       return $status; // nothing done
-               }
-
-               // At this point we are now comitted to returning an OK
-               // status unless some DB query error or other exception comes up.
-               // This way callers don't have to call rollback() if $status is bad
-               // unless they actually try to catch exceptions (which is rare).
-
-               // @TODO: pass content object?!
-               $revision = new Revision( [
-                       'page'       => $newid,
-                       'title'      => $this->mTitle, // for determining the default content model
-                       'comment'    => $summary,
-                       'minor_edit' => $meta['minor'],
-                       'text'       => $meta['serialized'],
-                       'len'        => $newsize,
-                       'user'       => $user->getId(),
-                       'user_text'  => $user->getName(),
-                       'timestamp'  => $now,
-                       'content_model' => $content->getModel(),
-                       'content_format' => $meta['serialFormat'],
-               ] );
-
-               // Save the revision text...
-               $revisionId = $revision->insertOn( $dbw );
-               // Update the page record with revision data
-               if ( !$this->updateRevisionOn( $dbw, $revision, 0 ) ) {
-                       throw new MWException( "Failed to update page row to use new revision." );
-               }
-
-               Hooks::run( 'NewRevisionFromEditComplete', [ $this, $revision, false, $user ] );
-
-               // Update recentchanges
-               if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
-                       // Mark as patrolled if the user can do so
-                       $patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) &&
-                               !count( $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
-                       // Add RC row to the DB
-                       RecentChange::notifyNew(
-                               $now,
-                               $this->mTitle,
-                               $revision->isMinor(),
-                               $user,
-                               $summary,
-                               $meta['bot'],
-                               '',
-                               $newsize,
-                               $revisionId,
-                               $patrolled,
-                               $meta['tags']
-                       );
-               }
-
-               $user->incEditCount();
-
-               if ( $wgPageCreationLog ) {
-                       // Log the page creation
-                       // @TODO: Do we want a 'recreate' action?
-                       $logEntry = new ManualLogEntry( 'create', 'create' );
-                       $logEntry->setPerformer( $user );
-                       $logEntry->setTarget( $this->mTitle );
-                       $logEntry->setComment( $summary );
-                       $logEntry->setTimestamp( $now );
-                       $logEntry->setAssociatedRevId( $revisionId );
-                       $logid = $logEntry->insert();
-                       // Note that we don't publish page creation events to recentchanges
-                       // (i.e. $logEntry->publish()) since this would create duplicate entries,
-                       // one for the edit and one for the page creation.
+               // $revRec will be null if the edit failed, or if no new revision was created because
+               // the content did not change.
+               if ( $revRec ) {
+                       // update cached fields
+                       // TODO: this is currently redundant to what is done in updateRevisionOn.
+                       // But updateRevisionOn() should move into PageStore, and then this will be needed.
+                       $this->setLastEdit( new Revision( $revRec ) ); // TODO: use RevisionRecord
+                       $this->mLatest = $revRec->getId();
                }
 
-               $dbw->endAtomic( __METHOD__ );
-               $this->mTimestamp = $now;
-
-               // Return the new revision to the caller
-               $status->value['revision'] = $revision;
-
-               // Do secondary updates once the main changes have been committed...
-               DeferredUpdates::addUpdate(
-                       new AtomicSectionUpdate(
-                               $dbw,
-                               __METHOD__,
-                               function () use (
-                                       $revision, &$user, $content, $summary, &$flags, $meta, &$status
-                               ) {
-                                       // Update links, etc.
-                                       $this->doEditUpdates( $revision, $user, [ 'created' => true ] );
-                                       // Avoid PHP 7.1 warning of passing $this by reference
-                                       $wikiPage = $this;
-                                       // Trigger post-create hook
-                                       $params = [ &$wikiPage, &$user, $content, $summary,
-                                                               $flags & EDIT_MINOR, null, null, &$flags, $revision ];
-                                       Hooks::run( 'PageContentInsertComplete', $params );
-                                       // Trigger post-save hook
-                                       $params = array_merge( $params, [ &$status, $meta['baseRevId'], 0 ] );
-                                       Hooks::run( 'PageContentSaveComplete', $params );
-                               }
-                       ),
-                       DeferredUpdates::PRESEND
-               );
-
-               return $status;
+               return $updater->getStatus();
        }
 
        /**
@@ -2027,14 +1857,17 @@ class WikiPage implements Page, IDBAccessObject {
        /**
         * Prepare content which is about to be saved.
         *
-        * Prior to 1.30, this returned a stdClass object with the same class
-        * members.
+        * Prior to 1.30, this returned a stdClass.
+        *
+        * @deprecated since 1.32, use getDerivedDataUpdater instead.
         *
         * @param Content $content
-        * @param Revision|int|null $revision Revision object. For backwards compatibility, a
-        *        revision ID is also accepted, but this is deprecated.
+        * @param Revision|RevisionRecord|int|null $revision Revision object.
+        *        For backwards compatibility, a revision ID is also accepted,
+        *        but this is deprecated.
+        *        Used with vary-revision or vary-revision-id.
         * @param User|null $user
-        * @param string|null $serialFormat
+        * @param string|null $serialFormat IGNORED
         * @param bool $useCache Check shared prepared edit cache
         *
         * @return PreparedEdit
@@ -2042,125 +1875,45 @@ class WikiPage implements Page, IDBAccessObject {
         * @since 1.21
         */
        public function prepareContentForEdit(
-               Content $content, $revision = null, User $user = null,
-               $serialFormat = null, $useCache = true
+               Content $content,
+               $revision = null,
+               User $user = null,
+               $serialFormat = null,
+               $useCache = true
        ) {
-               global $wgContLang, $wgUser, $wgAjaxEditStash;
+               global $wgUser;
 
-               if ( is_object( $revision ) ) {
-                       $revid = $revision->getId();
-               } else {
+               if ( !$user ) {
+                       $user = $wgUser;
+               }
+
+               if ( !is_object( $revision ) ) {
                        $revid = $revision;
                        // This code path is deprecated, and nothing is known to
                        // use it, so performance here shouldn't be a worry.
                        if ( $revid !== null ) {
                                wfDeprecated( __METHOD__ . ' with $revision = revision ID', '1.25' );
-                               $revision = Revision::newFromId( $revid, Revision::READ_LATEST );
+                               $store = $this->getRevisionStore();
+                               $revision = $store->getRevisionById( $revid, Revision::READ_LATEST );
                        } else {
                                $revision = null;
                        }
+               } elseif ( $revision instanceof Revision ) {
+                       $revision = $revision->getRevisionRecord();
                }
 
-               $user = is_null( $user ) ? $wgUser : $user;
-               // XXX: check $user->getId() here???
+               $slots = RevisionSlotsUpdate::newFromContent( [ 'main' => $content ] );
+               $updater = $this->getDerivedDataUpdater( $user, $revision, $slots );
 
-               // Use a sane default for $serialFormat, see T59026
-               if ( $serialFormat === null ) {
-                       $serialFormat = $content->getContentHandler()->getDefaultFormat();
-               }
+               if ( !$updater->isUpdatePrepared() ) {
+                       $updater->prepareContent( $user, $slots, [], $useCache );
 
-               if ( $this->mPreparedEdit
-                       && isset( $this->mPreparedEdit->newContent )
-                       && $this->mPreparedEdit->newContent->equals( $content )
-                       && $this->mPreparedEdit->revid == $revid
-                       && $this->mPreparedEdit->format == $serialFormat
-                       // XXX: also check $user here?
-               ) {
-                       // Already prepared
-                       return $this->mPreparedEdit;
-               }
-
-               // The edit may have already been prepared via api.php?action=stashedit
-               $cachedEdit = $useCache && $wgAjaxEditStash
-                       ? ApiStashEdit::checkCache( $this->getTitle(), $content, $user )
-                       : false;
-
-               $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
-               Hooks::run( 'ArticlePrepareTextForEdit', [ $this, $popts ] );
-
-               $edit = new PreparedEdit();
-               if ( $cachedEdit ) {
-                       $edit->timestamp = $cachedEdit->timestamp;
-               } else {
-                       $edit->timestamp = wfTimestampNow();
-               }
-               // @note: $cachedEdit is safely not used if the rev ID was referenced in the text
-               $edit->revid = $revid;
-
-               if ( $cachedEdit ) {
-                       $edit->pstContent = $cachedEdit->pstContent;
-               } else {
-                       $edit->pstContent = $content
-                               ? $content->preSaveTransform( $this->mTitle, $user, $popts )
-                               : null;
-               }
-
-               $edit->format = $serialFormat;
-               $edit->popts = $this->makeParserOptions( 'canonical' );
-               if ( $cachedEdit ) {
-                       $edit->output = $cachedEdit->output;
-               } else {
                        if ( $revision ) {
-                               // We get here if vary-revision is set. This means that this page references
-                               // itself (such as via self-transclusion). In this case, we need to make sure
-                               // that any such self-references refer to the newly-saved revision, and not
-                               // to the previous one, which could otherwise happen due to replica DB lag.
-                               $oldCallback = $edit->popts->getCurrentRevisionCallback();
-                               $edit->popts->setCurrentRevisionCallback(
-                                       function ( Title $title, $parser = false ) use ( $revision, &$oldCallback ) {
-                                               if ( $title->equals( $revision->getTitle() ) ) {
-                                                       return $revision;
-                                               } else {
-                                                       return call_user_func( $oldCallback, $title, $parser );
-                                               }
-                                       }
-                               );
-                       } else {
-                               // Try to avoid a second parse if {{REVISIONID}} is used
-                               $dbIndex = ( $this->mDataLoadedFrom & self::READ_LATEST ) === self::READ_LATEST
-                                       ? DB_MASTER // use the best possible guess
-                                       : DB_REPLICA; // T154554
-
-                               $edit->popts->setSpeculativeRevIdCallback( function () use ( $dbIndex ) {
-                                       $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
-                                       // Use a fresh connection in order to see the latest data, by avoiding
-                                       // stale data from REPEATABLE-READ snapshots.
-                                       $db = $lb->getConnectionRef( $dbIndex, [], false, $lb::CONN_TRX_AUTO );
-
-                                       return 1 + (int)$db->selectField(
-                                               'revision',
-                                               'MAX(rev_id)',
-                                               [],
-                                               __METHOD__
-                                       );
-                               } );
+                               $updater->prepareUpdate( $revision );
                        }
-                       $edit->output = $edit->pstContent
-                               ? $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts )
-                               : null;
                }
 
-               $edit->newContent = $content;
-               $edit->oldContent = $this->getContent( Revision::RAW );
-
-               if ( $edit->output ) {
-                       $edit->output->setCacheTime( wfTimestampNow() );
-               }
-
-               // Process cache the result
-               $this->mPreparedEdit = $edit;
-
-               return $edit;
+               return $updater->getPreparedEdit();
        }
 
        /**
@@ -2169,6 +1922,8 @@ class WikiPage implements Page, IDBAccessObject {
         * Purges pages that include this page if the text was changed here.
         * Every 100th edit, prune the recent changes table.
         *
+        * @deprecated since 1.32, use PageUpdater::doEditUpdates instead.
+        *
         * @param Revision $revision
         * @param User $user User object that did the revision
         * @param array $options Array of options, following indexes are used:
@@ -2185,165 +1940,13 @@ class WikiPage implements Page, IDBAccessObject {
         *   - 'no-change': don't update the article count, ever
         */
        public function doEditUpdates( Revision $revision, User $user, array $options = [] ) {
-               global $wgRCWatchCategoryMembership;
-
-               $options += [
-                       'changed' => true,
-                       'created' => false,
-                       'moved' => false,
-                       'restored' => false,
-                       'oldrevision' => null,
-                       'oldcountable' => null
-               ];
-               $content = $revision->getContent();
-
-               $logger = LoggerFactory::getInstance( 'SaveParse' );
-
-               // See if the parser output before $revision was inserted is still valid
-               $editInfo = false;
-               if ( !$this->mPreparedEdit ) {
-                       $logger->debug( __METHOD__ . ": No prepared edit...\n" );
-               } elseif ( $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
-                       $logger->info( __METHOD__ . ": Prepared edit has vary-revision...\n" );
-               } elseif ( $this->mPreparedEdit->output->getFlag( 'vary-revision-id' )
-                       && $this->mPreparedEdit->output->getSpeculativeRevIdUsed() !== $revision->getId()
-               ) {
-                       $logger->info( __METHOD__ . ": Prepared edit has vary-revision-id with wrong ID...\n" );
-               } elseif ( $this->mPreparedEdit->output->getFlag( 'vary-user' ) && !$options['changed'] ) {
-                       $logger->info( __METHOD__ . ": Prepared edit has vary-user and is null...\n" );
-               } else {
-                       wfDebug( __METHOD__ . ": Using prepared edit...\n" );
-                       $editInfo = $this->mPreparedEdit;
-               }
-
-               if ( !$editInfo ) {
-                       // Parse the text again if needed. Be careful not to do pre-save transform twice:
-                       // $text is usually already pre-save transformed once. Avoid using the edit stash
-                       // as any prepared content from there or in doEditContent() was already rejected.
-                       $editInfo = $this->prepareContentForEdit( $content, $revision, $user, null, false );
-               }
-
-               // Save it to the parser cache.
-               // Make sure the cache time matches page_touched to avoid double parsing.
-               MediaWikiServices::getInstance()->getParserCache()->save(
-                       $editInfo->output, $this, $editInfo->popts,
-                       $revision->getTimestamp(), $editInfo->revid
-               );
-
-               // Update the links tables and other secondary data
-               if ( $content ) {
-                       $recursive = $options['changed']; // T52785
-                       $updates = $content->getSecondaryDataUpdates(
-                               $this->getTitle(), null, $recursive, $editInfo->output
-                       );
-                       foreach ( $updates as $update ) {
-                               $update->setCause( 'edit-page', $user->getName() );
-                               if ( $update instanceof LinksUpdate ) {
-                                       $update->setRevision( $revision );
-                                       $update->setTriggeringUser( $user );
-                               }
-                               DeferredUpdates::addUpdate( $update );
-                       }
-                       if ( $wgRCWatchCategoryMembership
-                               && $this->getContentHandler()->supportsCategories() === true
-                               && ( $options['changed'] || $options['created'] )
-                               && !$options['restored']
-                       ) {
-                               // Note: jobs are pushed after deferred updates, so the job should be able to see
-                               // the recent change entry (also done via deferred updates) and carry over any
-                               // bot/deletion/IP flags, ect.
-                               JobQueueGroup::singleton()->lazyPush( new CategoryMembershipChangeJob(
-                                       $this->getTitle(),
-                                       [
-                                               'pageId' => $this->getId(),
-                                               'revTimestamp' => $revision->getTimestamp()
-                                       ]
-                               ) );
-                       }
-               }
-
-               // Avoid PHP 7.1 warning of passing $this by reference
-               $wikiPage = $this;
-
-               Hooks::run( 'ArticleEditUpdates', [ &$wikiPage, &$editInfo, $options['changed'] ] );
-
-               if ( Hooks::run( 'ArticleEditUpdatesDeleteFromRecentchanges', [ &$wikiPage ] ) ) {
-                       // Flush old entries from the `recentchanges` table
-                       if ( mt_rand( 0, 9 ) == 0 ) {
-                               JobQueueGroup::singleton()->lazyPush( RecentChangesUpdateJob::newPurgeJob() );
-                       }
-               }
-
-               if ( !$this->exists() ) {
-                       return;
-               }
-
-               $id = $this->getId();
-               $title = $this->mTitle->getPrefixedDBkey();
-               $shortTitle = $this->mTitle->getDBkey();
-
-               if ( $options['oldcountable'] === 'no-change' ||
-                       ( !$options['changed'] && !$options['moved'] )
-               ) {
-                       $good = 0;
-               } elseif ( $options['created'] ) {
-                       $good = (int)$this->isCountable( $editInfo );
-               } elseif ( $options['oldcountable'] !== null ) {
-                       $good = (int)$this->isCountable( $editInfo ) - (int)$options['oldcountable'];
-               } else {
-                       $good = 0;
-               }
-               $edits = $options['changed'] ? 1 : 0;
-               $pages = $options['created'] ? 1 : 0;
+               $revision = $revision->getRevisionRecord();
 
-               DeferredUpdates::addUpdate( SiteStatsUpdate::factory(
-                       [ 'edits' => $edits, 'articles' => $good, 'pages' => $pages ]
-               ) );
-               DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content ) );
-
-               // If this is another user's talk page, update newtalk.
-               // Don't do this if $options['changed'] = false (null-edits) nor if
-               // it's a minor edit and the user doesn't want notifications for those.
-               if ( $options['changed']
-                       && $this->mTitle->getNamespace() == NS_USER_TALK
-                       && $shortTitle != $user->getTitleKey()
-                       && !( $revision->isMinor() && $user->isAllowed( 'nominornewtalk' ) )
-               ) {
-                       $recipient = User::newFromName( $shortTitle, false );
-                       if ( !$recipient ) {
-                               wfDebug( __METHOD__ . ": invalid username\n" );
-                       } else {
-                               // Avoid PHP 7.1 warning of passing $this by reference
-                               $wikiPage = $this;
-
-                               // Allow extensions to prevent user notification
-                               // when a new message is added to their talk page
-                               if ( Hooks::run( 'ArticleEditUpdateNewTalk', [ &$wikiPage, $recipient ] ) ) {
-                                       if ( User::isIP( $shortTitle ) ) {
-                                               // An anonymous user
-                                               $recipient->setNewtalk( true, $revision );
-                                       } elseif ( $recipient->isLoggedIn() ) {
-                                               $recipient->setNewtalk( true, $revision );
-                                       } else {
-                                               wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" );
-                                       }
-                               }
-                       }
-               }
+               $updater = $this->getDerivedDataUpdater( $user, $revision );
 
-               if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
-                       MessageCache::singleton()->updateMessageOverride( $this->mTitle, $content );
-               }
+               $updater->prepareUpdate( $revision, $options );
 
-               if ( $options['created'] ) {
-                       self::onArticleCreate( $this->mTitle );
-               } elseif ( $options['changed'] ) { // T52785
-                       self::onArticleEdit( $this->mTitle, $revision );
-               }
-
-               ResourceLoaderWikiModule::invalidateModuleCache(
-                       $this->mTitle, $options['oldrevision'], $revision, wfWikiID()
-               );
+               $updater->doUpdates();
        }
 
        /**
@@ -2380,7 +1983,7 @@ class WikiPage implements Page, IDBAccessObject {
                // Take this opportunity to purge out expired restrictions
                Title::purgeExpiredRestrictions();
 
-               // @todo FIXME: Same limitations as described in ProtectionForm.php (line 37);
+               // @todo: Same limitations as described in ProtectionForm.php (line 37);
                // we expect a single selection, but the schema allows otherwise.
                $isProtected = false;
                $protect = false;
@@ -2808,7 +2411,7 @@ class WikiPage implements Page, IDBAccessObject {
                $tags = [], $logsubtype = 'delete'
        ) {
                global $wgUser, $wgContentHandlerUseDB, $wgCommentTableSchemaMigrationStage,
-                       $wgActorTableSchemaMigrationStage;
+                       $wgActorTableSchemaMigrationStage, $wgMultiContentRevisionSchemaMigrationStage;
 
                wfDebug( __METHOD__ . "\n" );
 
@@ -2895,9 +2498,17 @@ class WikiPage implements Page, IDBAccessObject {
                // Note array_intersect() preserves keys from the first arg, and we're
                // assuming $revQuery has `revision` primary and isn't using subtables
                // for anything we care about.
+               $tablesFlat = [];
+               array_walk_recursive(
+                       $revQuery['tables'],
+                       function ( $a ) use ( &$tablesFlat ) {
+                               $tablesFlat[] = $a;
+                       }
+               );
+
                $res = $dbw->select(
                        array_intersect(
-                               $revQuery['tables'],
+                               $tablesFlat,
                                [ 'revision', 'revision_comment_temp', 'revision_actor_temp' ]
                        ),
                        '1',
@@ -2937,14 +2548,29 @@ class WikiPage implements Page, IDBAccessObject {
                                'ar_minor_edit' => $row->rev_minor_edit,
                                'ar_rev_id'     => $row->rev_id,
                                'ar_parent_id'  => $row->rev_parent_id,
-                               'ar_text_id'    => $row->rev_text_id,
+                                       /**
+                                        * ar_text_id should probably not be written to when the multi content schema has
+                                        * been migrated to (wgMultiContentRevisionSchemaMigrationStage) however there is no
+                                        * default for the field in WMF production currently so we must keep writing
+                                        * writing until a default of 0 is set.
+                                        * Task: https://phabricator.wikimedia.org/T190148
+                                        * Copying the value from the revision table should not lead to any issues for now.
+                                        */
                                'ar_len'        => $row->rev_len,
                                'ar_page_id'    => $id,
                                'ar_deleted'    => $suppress ? $bitfield : $row->rev_deleted,
                                'ar_sha1'       => $row->rev_sha1,
                        ] + $commentStore->insert( $dbw, 'ar_comment', $comment )
                                + $actorMigration->getInsertValues( $dbw, 'ar_user', $user );
-                       if ( $wgContentHandlerUseDB ) {
+
+                       if ( $wgMultiContentRevisionSchemaMigrationStage < MIGRATION_NEW ) {
+                               $rowInsert['ar_text_id'] = $row->rev_text_id;
+                       }
+
+                       if (
+                               $wgContentHandlerUseDB &&
+                               $wgMultiContentRevisionSchemaMigrationStage <= MIGRATION_WRITE_BOTH
+                       ) {
                                $rowInsert['ar_content_model'] = $row->rev_content_model;
                                $rowInsert['ar_content_format'] = $row->rev_content_format;
                        }
@@ -3379,6 +3005,8 @@ class WikiPage implements Page, IDBAccessObject {
         * @param Title $title
         */
        public static function onArticleCreate( Title $title ) {
+               // TODO: move this into a PageEventEmitter service
+
                // Update existence markers on article/talk tabs...
                $other = $title->getOtherPage();
 
@@ -3410,6 +3038,8 @@ class WikiPage implements Page, IDBAccessObject {
         * @param Title $title
         */
        public static function onArticleDelete( Title $title ) {
+               // TODO: move this into a PageEventEmitter service
+
                // Update existence markers on article/talk tabs...
                // Clear Backlink cache first so that purge jobs use more up-to-date backlink information
                BacklinkCache::get( $title )->clear();
@@ -3455,12 +3085,24 @@ class WikiPage implements Page, IDBAccessObject {
         *
         * @param Title $title
         * @param Revision|null $revision Revision that was just saved, may be null
+        * @param string[]|null $slotsChanged The role names of the slots that were changed.
+        *        If not given, all slots are assumed to have changed.
         */
-       public static function onArticleEdit( Title $title, Revision $revision = null ) {
-               // Invalidate caches of articles which include this page
-               DeferredUpdates::addUpdate(
-                       new HTMLCacheUpdate( $title, 'templatelinks', 'page-edit' )
-               );
+       public static function onArticleEdit(
+               Title $title,
+               Revision $revision = null,
+               $slotsChanged = null
+       ) {
+               // TODO: move this into a PageEventEmitter service
+
+               if ( $slotsChanged === null || in_array( 'main',  $slotsChanged ) ) {
+                       // Invalidate caches of articles which include this page.
+                       // Only for the main slot, because only the main slot is transcluded.
+                       // TODO: MCR: not true for TemplateStyles! [SlotHandler]
+                       DeferredUpdates::addUpdate(
+                               new HTMLCacheUpdate( $title, 'templatelinks', 'page-edit' )
+                       );
+               }
 
                // Invalidate the caches of all pages which redirect here
                DeferredUpdates::addUpdate(
@@ -3781,4 +3423,5 @@ class WikiPage implements Page, IDBAccessObject {
 
                return $linkCache->getMutableCacheKeys( $cache, $this->getTitle()->getTitleValue() );
        }
+
 }
index 1ff8859..99ca07a 100644 (file)
@@ -449,7 +449,7 @@ class CoreParserFunctions {
                                $parser->mOutput->setDisplayTitle( $text );
                        }
                        if ( $old !== false && $old !== $text && !$arg ) {
-                               $converter = $parser->getConverterLanguage()->getConverter();
+                               $converter = $parser->getTargetLanguage()->getConverter();
                                return '<span class="error">' .
                                        wfMessage( 'duplicate-displaytitle',
                                                // Message should be parsed, but these params should only be escaped.
@@ -461,7 +461,7 @@ class CoreParserFunctions {
                                return '';
                        }
                } else {
-                       $converter = $parser->getConverterLanguage()->getConverter();
+                       $converter = $parser->getTargetLanguage()->getConverter();
                        $parser->getOutput()->addWarning(
                                wfMessage( 'restricted-displaytitle',
                                        // Message should be parsed, but this param should only be escaped.
@@ -982,7 +982,7 @@ class CoreParserFunctions {
                if ( $old === false || $old == $text || $arg ) {
                        return '';
                } else {
-                       $converter = $parser->getConverterLanguage()->getConverter();
+                       $converter = $parser->getTargetLanguage()->getConverter();
                        return '<span class="error">' .
                                wfMessage( 'duplicate-defaultsort',
                                        // Message should be parsed, but these params should only be escaped.
@@ -1162,7 +1162,7 @@ class CoreParserFunctions {
                }
 
                // Check the link cache, maybe something already looked it up.
-               $linkCache = LinkCache::singleton();
+               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                $pdbk = $t->getPrefixedDBkey();
                $id = $linkCache->getGoodLinkID( $pdbk );
                if ( $id != 0 ) {
index 1d722c2..939fe73 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup Parser
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * @ingroup Parser
  */
@@ -283,7 +285,7 @@ class LinkHolderArray {
                global $wgContLang;
 
                $colours = [];
-               $linkCache = LinkCache::singleton();
+               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                $output = $this->parent->getOutput();
                $linkRenderer = $this->parent->getLinkRenderer();
 
@@ -451,7 +453,7 @@ class LinkHolderArray {
                $linkBatch = new LinkBatch();
                $variantMap = []; // maps $pdbkey_Variant => $keys (of link holders)
                $output = $this->parent->getOutput();
-               $linkCache = LinkCache::singleton();
+               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                $titlesToBeConverted = '';
                $titlesAttrs = [];
 
index 28e7450..a1b3064 100644 (file)
@@ -460,11 +460,11 @@ class Parser {
                        || isset( $this->mDoubleUnderscores['notitleconvert'] )
                        || $this->mOutput->getDisplayTitle() !== false )
                ) {
-                       $convruletitle = $this->getConverterLanguage()->getConvRuleTitle();
+                       $convruletitle = $this->getTargetLanguage()->getConvRuleTitle();
                        if ( $convruletitle ) {
                                $this->mOutput->setTitleText( $convruletitle );
                        } else {
-                               $titleText = $this->getConverterLanguage()->convertTitle( $title );
+                               $titleText = $this->getTargetLanguage()->convertTitle( $title );
                                $this->mOutput->setTitleText( $titleText );
                        }
                }
@@ -897,6 +897,7 @@ class Parser {
 
        /**
         * Get the language object for language conversion
+        * @deprecated since 1.32, just use getTargetLanguage()
         * @return Language|null
         */
        public function getConverterLanguage() {
@@ -1380,7 +1381,7 @@ class Parser {
                                # The position of the convert() call should not be changed. it
                                # assumes that the links are all replaced and the only thing left
                                # is the <nowiki> mark.
-                               $text = $this->getConverterLanguage()->convert( $text );
+                               $text = $this->getTargetLanguage()->convert( $text );
                        }
                }
 
@@ -1605,7 +1606,7 @@ class Parser {
                if ( $text === false ) {
                        # Not an image, make a link
                        $text = Linker::makeExternalLink( $url,
-                               $this->getConverterLanguage()->markNoConversion( $url, true ),
+                               $this->getTargetLanguage()->getConverter()->markNoConversion( $url ),
                                true, 'free',
                                $this->getExternalLinkAttribs( $url ), $this->mTitle );
                        # Register it in the output object...
@@ -1894,7 +1895,10 @@ class Parser {
                                list( $dtrail, $trail ) = Linker::splitTrail( $trail );
                        }
 
-                       $text = $this->getConverterLanguage()->markNoConversion( $text );
+                       // Excluding protocol-relative URLs may avoid many false positives.
+                       if ( preg_match( '/^(?:' . wfUrlProtocolsWithoutProtRel() . ')/', $text ) ) {
+                               $text = $this->getTargetLanguage()->getConverter()->markNoConversion( $text );
+                       }
 
                        $url = Sanitizer::cleanUrl( $url );
 
@@ -2360,7 +2364,7 @@ class Parser {
                                        }
                                        $sortkey = Sanitizer::decodeCharReferences( $sortkey );
                                        $sortkey = str_replace( "\n", '', $sortkey );
-                                       $sortkey = $this->getConverterLanguage()->convertCategoryKey( $sortkey );
+                                       $sortkey = $this->getTargetLanguage()->convertCategoryKey( $sortkey );
                                        $this->mOutput->addCategory( $nt->getDBkey(), $sortkey );
 
                                        continue;
@@ -3185,8 +3189,8 @@ class Parser {
                        if ( $title ) {
                                $titleText = $title->getPrefixedText();
                                # Check for language variants if the template is not found
-                               if ( $this->getConverterLanguage()->hasVariants() && $title->getArticleID() == 0 ) {
-                                       $this->getConverterLanguage()->findVariantLink( $part1, $title, true );
+                               if ( $this->getTargetLanguage()->hasVariants() && $title->getArticleID() == 0 ) {
+                                       $this->getTargetLanguage()->findVariantLink( $part1, $title, true );
                                }
                                # Do recursion depth check
                                $limit = $this->mOptions->getMaxTemplateDepth();
@@ -3631,7 +3635,7 @@ class Parser {
                        $rev_id = $rev ? $rev->getId() : 0;
                        # If there is no current revision, there is no page
                        if ( $id === false && !$rev ) {
-                               $linkCache = LinkCache::singleton();
+                               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
                                $linkCache->addBadLinkObj( $title );
                        }
 
index 20bd599..5959281 100644 (file)
@@ -20,6 +20,8 @@
  * @file
  * @ingroup Parser
  */
+
+use MediaWiki\MediaWikiServices;
 use Wikimedia\ScopedCallback;
 
 /**
@@ -1386,11 +1388,12 @@ class ParserOptions {
                        };
                end( $wgHooks['TitleExists'] );
                $key = key( $wgHooks['TitleExists'] );
-               LinkCache::singleton()->clearBadLink( $title->getPrefixedDBkey() );
-               return new ScopedCallback( function () use ( $title, $key ) {
+               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
+               $linkCache->clearBadLink( $title->getPrefixedDBkey() );
+               return new ScopedCallback( function () use ( $title, $key, $linkCache ) {
                        global $wgHooks;
                        unset( $wgHooks['TitleExists'][$key] );
-                       LinkCache::singleton()->clearLink( $title );
+                       $linkCache->clearLink( $title );
                } );
        }
 }
index eed4b0d..bf6ab4a 100644 (file)
@@ -406,7 +406,7 @@ class DefaultPreferencesFactory implements PreferencesFactory {
                }
 
                // Language
-               $languages = Language::fetchLanguageNames( null, 'mw' );
+               $languages = Language::fetchLanguageNames( null, 'mwfile' );
                $languageCode = $this->config->get( 'LanguageCode' );
                if ( !array_key_exists( $languageCode, $languages ) ) {
                        $languages[$languageCode] = $languageCode;
index b9ff732..20faf8b 100644 (file)
@@ -95,7 +95,6 @@ class ResourceLoaderClientHtml {
        /**
         * Ensure the styles of one or more modules are loaded.
         *
-        * @deprecated since 1.28
         * @param array $modules Array of module names
         */
        public function setModuleStyles( array $modules ) {
index 53ae435..ff5de0d 100644 (file)
@@ -534,6 +534,8 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule {
        ) {
                static $formats = [ CONTENT_FORMAT_CSS, CONTENT_FORMAT_JAVASCRIPT ];
 
+               // TODO: MCR: differentiate between page functionality and content model!
+               //       Not all pages containing CSS or JS have to be modules! [PageType]
                if ( $old && in_array( $old->getContentFormat(), $formats ) ) {
                        $purge = true;
                } elseif ( $new && in_array( $new->getContentFormat(), $formats ) ) {
diff --git a/includes/search/PaginatingSearchEngine.php b/includes/search/PaginatingSearchEngine.php
new file mode 100644 (file)
index 0000000..97ef2d5
--- /dev/null
@@ -0,0 +1,12 @@
+<?php
+
+/**
+ * Marker class for search engines that can handle their own pagination, by
+ * reporting in their SearchResultSet when a next page is available. This
+ * only applies to search{Title,Text} and not to completion search.
+ *
+ * SearchEngine implementations not implementing this interface will have
+ * an over-fetch performed to determine next page availability.
+ */
+interface PaginatingSearchEngine {
+}
index bd48e21..b2bacda 100644 (file)
@@ -77,7 +77,9 @@ abstract class SearchEngine {
         * @return SearchResultSet|Status|null
         */
        public function searchText( $term ) {
-               return $this->doSearchText( $term );
+               return $this->maybePaginate( function () use ( $term ) {
+                       return $this->doSearchText( $term );
+               } );
        }
 
        /**
@@ -132,7 +134,9 @@ abstract class SearchEngine {
         * @return SearchResultSet|null
         */
        public function searchTitle( $term ) {
-               return $this->doSearchTitle( $term );
+               return $this->maybePaginate( function () use ( $term ) {
+                       return $this->doSearchTitle( $term );
+               } );
        }
 
        /**
@@ -146,6 +150,40 @@ abstract class SearchEngine {
                return null;
        }
 
+       /**
+        * Performs an overfetch and shrink operation to determine if
+        * the next page is available for search engines that do not
+        * explicitly implement their own pagination.
+        *
+        * @param Closure $fn Takes no arguments
+        * @return SearchResultSet|Status<SearchResultSet>|null Result of calling $fn
+        */
+       private function maybePaginate( Closure $fn ) {
+               if ( $this instanceof PaginatingSearchEngine ) {
+                       return $fn();
+               }
+               $this->limit++;
+               try {
+                       $resultSetOrStatus = $fn();
+               } finally {
+                       $this->limit--;
+               }
+
+               $resultSet = null;
+               if ( $resultSetOrStatus instanceof SearchResultSet ) {
+                       $resultSet = $resultSetOrStatus;
+               } elseif ( $resultSetOrStatus instanceof Status &&
+                       $resultSetOrStatus->getValue() instanceof SearchResultSet
+               ) {
+                       $resultSet = $resultSetOrStatus->getValue();
+               }
+               if ( $resultSet ) {
+                       $resultSet->shrink( $this->limit );
+               }
+
+               return $resultSetOrStatus;
+       }
+
        /**
         * @since 1.18
         * @param string $feature
@@ -523,6 +561,22 @@ abstract class SearchEngine {
                return $search;
        }
 
+       /**
+        * Perform an overfetch of completion search results. This allows
+        * determining if another page of results is available.
+        *
+        * @param string $search
+        * @return SearchSuggestionSet
+        */
+       protected function completionSearchBackendOverfetch( $search ) {
+               $this->limit++;
+               try {
+                       return $this->completionSearchBackend( $search );
+               } finally {
+                       $this->limit--;
+               }
+       }
+
        /**
         * Perform a completion search.
         * Does not resolve namespaces and does not check variants.
@@ -560,7 +614,8 @@ abstract class SearchEngine {
                        return SearchSuggestionSet::emptySuggestionSet(); // Return empty result
                }
                $search = $this->normalizeNamespaces( $search );
-               return $this->processCompletionResults( $search, $this->completionSearchBackend( $search ) );
+               $suggestions = $this->completionSearchBackendOverfetch( $search );
+               return $this->processCompletionResults( $search, $suggestions );
        }
 
        /**
@@ -574,8 +629,8 @@ abstract class SearchEngine {
                }
                $search = $this->normalizeNamespaces( $search );
 
-               $results = $this->completionSearchBackend( $search );
-               $fallbackLimit = $this->limit - $results->getSize();
+               $results = $this->completionSearchBackendOverfetch( $search );
+               $fallbackLimit = 1 + $this->limit - $results->getSize();
                if ( $fallbackLimit > 0 ) {
                        global $wgContLang;
 
@@ -614,15 +669,26 @@ abstract class SearchEngine {
         * @return SearchSuggestionSet
         */
        protected function processCompletionResults( $search, SearchSuggestionSet $suggestions ) {
+               // We over-fetched to determine pagination. Shrink back down if we have extra results
+               // and mark if pagination is possible
+               $suggestions->shrink( $this->limit );
+
                $search = trim( $search );
                // preload the titles with LinkBatch
-               $titles = $suggestions->map( function ( SearchSuggestion $sugg ) {
+               $lb = new LinkBatch( $suggestions->map( function ( SearchSuggestion $sugg ) {
                        return $sugg->getSuggestedTitle();
-               } );
-               $lb = new LinkBatch( $titles );
+               } ) );
                $lb->setCaller( __METHOD__ );
                $lb->execute();
 
+               $diff = $suggestions->filter( function ( SearchSuggestion $sugg ) {
+                       return $sugg->getSuggestedTitle()->isKnown();
+               } );
+               if ( $diff > 0 ) {
+                       MediaWikiServices::getInstance()->getStatsdDataFactory()
+                               ->updateCount( 'search.completion.missing', $diff );
+               }
+
                $results = $suggestions->map( function ( SearchSuggestion $sugg ) {
                        return $sugg->getSuggestedTitle()->getPrefixedText();
                } );
@@ -830,7 +896,6 @@ abstract class SearchEngine {
                $setAugmentors = [];
                $rowAugmentors = [];
                Hooks::run( "SearchResultsAugment", [ &$setAugmentors, &$rowAugmentors ] );
-
                if ( !$setAugmentors && !$rowAugmentors ) {
                        // We're done here
                        return;
index 3141797..42bc62d 100644 (file)
@@ -3,28 +3,18 @@
  * A SearchResultSet wrapper for SearchNearMatcher
  */
 class SearchNearMatchResultSet extends SearchResultSet {
-       private $fetched = false;
-
        /**
         * @param Title|null $match Title if matched, else null
         */
        public function __construct( $match ) {
-               $this->result = $match;
-       }
-
-       public function numRows() {
-               return $this->result ? 1 : 0;
-       }
-
-       public function next() {
-               if ( $this->fetched || !$this->result ) {
-                       return false;
+               if ( $match === null ) {
+                       $this->results = [];
+               } else {
+                       $this->results = [ SearchResult::newFromTitle( $match, $this ) ];
                }
-               $this->fetched = true;
-               return SearchResult::newFromTitle( $this->result, $this );
        }
 
-       public function rewind() {
-               $this->fetched = false;
+       public function numRows() {
+               return $this->results ? 1 : 0;
        }
 }
index dc294c3..2f20d9d 100644 (file)
@@ -57,8 +57,8 @@ class SearchResult {
        protected $searchEngine;
 
        /**
-        * A set of extension data.
-        * @var array[]
+        * A function returning a set of extension data.
+        * @var Closure|null
         */
        protected $extensionData;
 
@@ -267,17 +267,34 @@ class SearchResult {
         * @return array[]
         */
        public function getExtensionData() {
-               return $this->extensionData;
+               if ( $this->extensionData ) {
+                       return call_user_func( $this->extensionData );
+               } else {
+                       return [];
+               }
        }
 
        /**
         * Set extension data for this result.
         * The data is:
         * augmentor name => data
-        * @param array[] $extensionData
+        * @param Closure|array $extensionData Takes no arguments, returns
+        *  either array of extension data or null.
         */
-       public function setExtensionData( array $extensionData ) {
-               $this->extensionData = $extensionData;
+       public function setExtensionData( $extensionData ) {
+               if ( $extensionData instanceof Closure ) {
+                       $this->extensionData = $extensionData;
+               } elseif ( is_array( $extensionData ) ) {
+                       wfDeprecated( __METHOD__ . ' with array argument', 1.32 );
+                       $this->extensionData = function () use ( $extensionData ) {
+                               return $extensionData;
+                       };
+               } else {
+                       $type = is_object( $extensionData )
+                               ? get_class( $extensionData )
+                               : gettype( $extensionData );
+                       throw new \InvalidArgumentException(
+                               __METHOD__ . " must be called with Closure|array, but received $type" );
+               }
        }
-
 }
index e3eb4c2..5728a52 100644 (file)
@@ -24,8 +24,7 @@
 /**
  * @ingroup Search
  */
-class SearchResultSet {
-
+class SearchResultSet implements Countable, IteratorAggregate {
        /**
         * Types of interwiki results
         */
@@ -54,7 +53,7 @@ class SearchResultSet {
         * as an array.
         * @var SearchResult[]
         */
-       private $results;
+       protected $results;
 
        /**
         * Set of result's extra data, indexed per result id
@@ -65,8 +64,31 @@ class SearchResultSet {
         */
        protected $extraData = [];
 
-       public function __construct( $containedSyntax = false ) {
+       /**
+        * @var boolean True when there are more pages of search results available.
+        */
+       private $hasMoreResults;
+
+       /**
+        * @var ArrayIterator|null Iterator supporting BC iteration methods
+        */
+       private $bcIterator;
+
+       /**
+        * @param bool $containedSyntax True when query is not requesting a simple
+        *  term match
+        * @param bool $hasMoreResults True when there are more pages of search
+        *  results available.
+        */
+       public function __construct( $containedSyntax = false, $hasMoreResults = false ) {
+               if ( static::class === __CLASS__ ) {
+                       // This class will eventually be abstract. SearchEngine implementations
+                       // already have to extend this class anyways to provide the actual
+                       // search results.
+                       wfDeprecated( __METHOD__, 1.32 );
+               }
                $this->containedSyntax = $containedSyntax;
+               $this->hasMoreResults = $hasMoreResults;
        }
 
        /**
@@ -81,7 +103,11 @@ class SearchResultSet {
        }
 
        function numRows() {
-               return 0;
+               return $this->count();
+       }
+
+       final public function count() {
+               return count( $this->extractResults() );
        }
 
        /**
@@ -171,20 +197,39 @@ class SearchResultSet {
 
        /**
         * Fetches next search result, or false.
-        * STUB
-        * FIXME: refactor as iterator, so we could use nicer interfaces.
-        * @deprecated since 1.32; Use self::extractResults()
+        * @deprecated since 1.32; Use self::extractResults() or foreach
         * @return SearchResult|false
         */
-       function next() {
-               return false;
+       public function next() {
+               wfDeprecated( __METHOD__, '1.32' );
+               $it = $this->bcIterator();
+               $searchResult = $it->current();
+               $it->next();
+               return $searchResult === null ? false : $searchResult;
        }
 
        /**
         * Rewind result set back to beginning
-        * @deprecated since 1.32; Use self::extractResults()
+        * @deprecated since 1.32; Use self::extractResults() or foreach
         */
-       function rewind() {
+       public function rewind() {
+               wfDeprecated( __METHOD__, '1.32' );
+               $this->bcIterator()->rewind();
+       }
+
+       private function bcIterator() {
+               if ( $this->bcIterator === null ) {
+                       $this->bcIterator = 'RECURSION';
+                       $this->bcIterator = $this->getIterator();
+               } elseif ( $this->bcIterator === 'RECURSION' ) {
+                       // Either next/rewind or extractResults must be implemented.  This
+                       // class was potentially instantiated directly. It should be
+                       // abstract with abstract methods to enforce this but that's a
+                       // breaking change...
+                       wfDeprecated( static::class . ' without implementing extractResults', '1.32' );
+                       $this->bcIterator = new ArrayIterator( [] );
+               }
+               return $this->bcIterator;
        }
 
        /**
@@ -204,6 +249,34 @@ class SearchResultSet {
                return $this->containedSyntax;
        }
 
+       /**
+        * @return bool True when there are more pages of search results available.
+        */
+       public function hasMoreResults() {
+               return $this->hasMoreResults;
+       }
+
+       /**
+        * @param int $limit Shrink result set to $limit and flag
+        *  if more results are available.
+        */
+       public function shrink( $limit ) {
+               if ( $this->count() > $limit ) {
+                       $this->hasMoreResults = true;
+                       // shrinking result set for implementations that
+                       // have not implemented extractResults and use
+                       // the default cache location. Other implementations
+                       // must override this as well.
+                       if ( is_array( $this->results ) ) {
+                               $this->results = array_slice( $this->results, 0, $limit );
+                       } else {
+                               throw new \UnexpectedValueException(
+                                       "When overriding result store extending classes must "
+                                       . " also override " . __METHOD__ );
+                       }
+               }
+       }
+
        /**
         * Extract all the results in the result set as array.
         * @return SearchResult[]
@@ -258,15 +331,19 @@ class SearchResultSet {
        /**
         * Returns extra data for specific result and store it in SearchResult object.
         * @param SearchResult $result
-        * @return array|null List of data as name => value or null if none present.
         */
        public function augmentResult( SearchResult $result ) {
                $id = $result->getTitle()->getArticleID();
-               if ( !$id || !isset( $this->extraData[$id] ) ) {
-                       return null;
+               if ( $id === -1 ) {
+                       return;
                }
-               $result->setExtensionData( $this->extraData[$id] );
-               return $this->extraData[$id];
+               $result->setExtensionData( function () use ( $id ) {
+                       if ( isset( $this->extraData[$id] ) ) {
+                               return $this->extraData[$id];
+                       } else {
+                               return [];
+                       }
+               } );
        }
 
        /**
@@ -278,4 +355,8 @@ class SearchResultSet {
        public function getOffset() {
                return null;
        }
+
+       final public function getIterator() {
+               return new ArrayIterator( $this->extractResults() );
+       }
 }
index aced5e1..cb1f831 100644 (file)
@@ -35,6 +35,11 @@ class SearchSuggestionSet {
         */
        private $pageMap = [];
 
+       /**
+        * @var bool Are more results available?
+        */
+       private $hasMoreResults;
+
        /**
         * Builds a new set of suggestions.
         *
@@ -45,8 +50,10 @@ class SearchSuggestionSet {
         * unexpected behaviors.
         *
         * @param SearchSuggestion[] $suggestions (must be sorted by score)
+        * @param bool $hasMoreResults Are more results available?
         */
-       public function __construct( array $suggestions ) {
+       public function __construct( array $suggestions, $hasMoreResults = false ) {
+               $this->hasMoreResults = $hasMoreResults;
                foreach ( $suggestions as $suggestion ) {
                        $pageID = $suggestion->getSuggestedTitleID();
                        if ( $pageID && empty( $this->pageMap[$pageID] ) ) {
@@ -56,6 +63,13 @@ class SearchSuggestionSet {
                }
        }
 
+       /**
+        * @return bool Are more results available?
+        */
+       public function hasMoreResults() {
+               return $this->hasMoreResults;
+       }
+
        /**
         * Get the list of suggestions.
         * @return SearchSuggestion[]
@@ -73,6 +87,18 @@ class SearchSuggestionSet {
                return array_map( $callback, $this->suggestions );
        }
 
+       /**
+        * Filter the suggestions array
+        * @param callback $callback Callable accepting single SearchSuggestion
+        *  instance returning bool false to remove the item.
+        * @return int The number of suggestions removed
+        */
+       public function filter( $callback ) {
+               $before = count( $this->suggestions );
+               $this->suggestions = array_values( array_filter( $this->suggestions, $callback ) );
+               return $before - count( $this->suggestions );
+       }
+
        /**
         * Add a new suggestion at the end.
         * If the score of the new suggestion is greater than the worst one,
@@ -167,6 +193,7 @@ class SearchSuggestionSet {
        public function shrink( $limit ) {
                if ( count( $this->suggestions ) > $limit ) {
                        $this->suggestions = array_slice( $this->suggestions, 0, $limit );
+                       $this->hasMoreResults = true;
                }
        }
 
@@ -177,14 +204,15 @@ class SearchSuggestionSet {
         * NOTE: Suggestion scores will be generated.
         *
         * @param Title[] $titles
+        * @param bool $hasMoreResults Are more results available?
         * @return SearchSuggestionSet
         */
-       public static function fromTitles( array $titles ) {
+       public static function fromTitles( array $titles, $hasMoreResults = false ) {
                $score = count( $titles );
                $suggestions = array_map( function ( $title ) use ( &$score ) {
                        return SearchSuggestion::fromTitle( $score--, $title );
                }, $titles );
-               return new SearchSuggestionSet( $suggestions );
+               return new SearchSuggestionSet( $suggestions, $hasMoreResults );
        }
 
        /**
@@ -193,14 +221,15 @@ class SearchSuggestionSet {
         * NOTE: Suggestion scores will be generated.
         *
         * @param string[] $titles
+        * @param bool $hasMoreResults Are more results available?
         * @return SearchSuggestionSet
         */
-       public static function fromStrings( array $titles ) {
+       public static function fromStrings( array $titles, $hasMoreResults = false ) {
                $score = count( $titles );
                $suggestions = array_map( function ( $title ) use ( &$score ) {
                        return SearchSuggestion::fromText( $score--, $title );
                }, $titles );
-               return new SearchSuggestionSet( $suggestions );
+               return new SearchSuggestionSet( $suggestions, $hasMoreResults );
        }
 
        /**
index 53d09e8..022dc0a 100644 (file)
@@ -7,8 +7,11 @@ use Wikimedia\Rdbms\ResultWrapper;
  * @ingroup Search
  */
 class SqlSearchResultSet extends SearchResultSet {
+       /** @var ResultWrapper Result object from database */
        protected $resultSet;
+       /** @var string Requested search query */
        protected $terms;
+       /** @var int|null Total number of hits for $terms */
        protected $totalHits;
 
        function __construct( ResultWrapper $resultSet, $terms, $total = null ) {
@@ -29,25 +32,21 @@ class SqlSearchResultSet extends SearchResultSet {
                return $this->resultSet->numRows();
        }
 
-       function next() {
+       public function extractResults() {
                if ( $this->resultSet === false ) {
-                       return false;
-               }
-
-               $row = $this->resultSet->fetchObject();
-               if ( $row === false ) {
-                       return false;
+                       return [];
                }
 
-               return SearchResult::newFromTitle(
-                       Title::makeTitle( $row->page_namespace, $row->page_title ), $this
-               );
-       }
-
-       function rewind() {
-               if ( $this->resultSet ) {
+               if ( $this->results === null ) {
+                       $this->results = [];
                        $this->resultSet->rewind();
+                       while ( ( $row = $this->resultSet->fetchObject() ) !== false ) {
+                               $this->results[] = SearchResult::newFromTitle(
+                                       Title::makeTitle( $row->page_namespace, $row->page_title ), $this
+                               );
+                       }
                }
+               return $this->results;
        }
 
        function free() {
index c98b7da..5ea49ee 100644 (file)
@@ -356,9 +356,10 @@ class ServiceContainer implements DestructibleService {
        private function createService( $name ) {
                if ( isset( $this->serviceInstantiators[$name] ) ) {
                        $service = ( $this->serviceInstantiators[$name] )(
-                               $this, ...$this->extraInstantiationParams
+                               $this,
+                               ...$this->extraInstantiationParams
                        );
-                       // NOTE: when adding more wiring logic here, make sure copyWiring() is kept in sync!
+                       // NOTE: when adding more wiring logic here, make sure importWiring() is kept in sync!
                } else {
                        throw new NoSuchServiceException( $name );
                }
index d029163..157cc52 100644 (file)
@@ -122,22 +122,28 @@ class PHPSessionHandler implements \SessionHandlerInterface {
                // Close any auto-started session, before we replace it
                session_write_close();
 
-               // Tell PHP not to mess with cookies itself
-               ini_set( 'session.use_cookies', 0 );
-               ini_set( 'session.use_trans_sid', 0 );
-
-               // T124510: Disable automatic PHP session related cache headers.
-               // MediaWiki adds it's own headers and the default PHP behavior may
-               // set headers such as 'Pragma: no-cache' that cause problems with
-               // some user agents.
-               session_cache_limiter( '' );
-
-               // Also set a sane serialization handler
-               \Wikimedia\PhpSessionSerializer::setSerializeHandler();
-
-               // Register this as the save handler, and register an appropriate
-               // shutdown function.
-               session_set_save_handler( self::$instance, true );
+               try {
+                       \Wikimedia\suppressWarnings();
+
+                       // Tell PHP not to mess with cookies itself
+                       ini_set( 'session.use_cookies', 0 );
+                       ini_set( 'session.use_trans_sid', 0 );
+
+                       // T124510: Disable automatic PHP session related cache headers.
+                       // MediaWiki adds it's own headers and the default PHP behavior may
+                       // set headers such as 'Pragma: no-cache' that cause problems with
+                       // some user agents.
+                       session_cache_limiter( '' );
+
+                       // Also set a sane serialization handler
+                       \Wikimedia\PhpSessionSerializer::setSerializeHandler();
+
+                       // Register this as the save handler, and register an appropriate
+                       // shutdown function.
+                       session_set_save_handler( self::$instance, true );
+               } finally {
+                       \Wikimedia\restoreWarnings();
+               }
        }
 
        /**
index aa20e20..296c133 100644 (file)
@@ -77,11 +77,7 @@ abstract class QuickTemplate {
         * @return mixed The value of the data requested or the deafult
         */
        public function get( $name, $default = null ) {
-               if ( isset( $this->data[$name] ) ) {
-                       return $this->data[$name];
-               } else {
-                       return $default;
-               }
+               return $this->data[$name] ?? $default;
        }
 
        /**
index 55d3462..8384ca0 100644 (file)
@@ -220,7 +220,6 @@ abstract class Skin extends ContextSource {
                // Preload jquery.tablesorter for mediawiki.page.ready
                if ( strpos( $out->getHTML(), 'sortable' ) !== false ) {
                        $modules['content'][] = 'jquery.tablesorter';
-                       $modules['styles']['content'][] = 'jquery.tablesorter.styles';
                }
 
                // Preload jquery.makeCollapsible for mediawiki.page.ready
index 28f04fa..d6fb10f 100644 (file)
@@ -59,6 +59,7 @@ class SpecialComparePages extends SpecialPage {
                                'size' => '40',
                                'section' => 'page1',
                                'validation-callback' => [ $this, 'checkExistingTitle' ],
+                               'required' => false,
                        ],
                        'Revision1' => [
                                'type' => 'int',
@@ -75,6 +76,7 @@ class SpecialComparePages extends SpecialPage {
                                'size' => '40',
                                'section' => 'page2',
                                'validation-callback' => [ $this, 'checkExistingTitle' ],
+                               'required' => false,
                        ],
                        'Revision2' => [
                                'type' => 'int',
index e25b11e..e55ab1f 100644 (file)
@@ -52,7 +52,7 @@
  */
 class UploadStash {
        // Format of the key for files -- has to be suitable as a filename itself (e.g. ab12cd34ef.jpg)
-       const KEY_FORMAT_REGEX = '/^[\w-\.]+\.\w*$/';
+       const KEY_FORMAT_REGEX = '/^[\w\-\.]+\.\w*$/';
        const MAX_US_PROPS_SIZE = 65535;
 
        /**
index c5fa05a..4e6e83a 100644 (file)
@@ -636,6 +636,27 @@ class User implements IDBAccessObject, UserIdentity {
                return $u;
        }
 
+       /**
+        * Returns a User object corresponding to the given UserIdentity.
+        *
+        * @since 1.32
+        *
+        * @param UserIdentity $identity
+        *
+        * @return User
+        */
+       public static function newFromIdentity( UserIdentity $identity ) {
+               if ( $identity instanceof User ) {
+                       return $identity;
+               }
+
+               return self::newFromAnyId(
+                       $identity->getId() === 0 ? null : $identity->getId(),
+                       $identity->getName() === '' ? null : $identity->getName(),
+                       $identity->getActorId() === 0 ? null : $identity->getActorId()
+               );
+       }
+
        /**
         * Static factory method for creation from an ID, name, and/or actor ID
         *
@@ -5497,6 +5518,11 @@ class User implements IDBAccessObject, UserIdentity {
                        }
                }
 
+               // Replace deprecated language codes
+               $this->mOptions['language'] = LanguageCode::replaceDeprecatedCodes(
+                       $this->mOptions['language']
+               );
+
                $this->mOptionsLoaded = true;
 
                Hooks::run( 'UserLoadOptions', [ $this, &$this->mOptions ] );
index e236640..8521e68 100644 (file)
@@ -123,10 +123,8 @@ class BasicSearchResultSetWidget {
                $terms = $wgContLang->convertForSearchResult( $resultSet->termMatches() );
 
                $hits = [];
-               $result = $resultSet->next();
-               while ( $result ) {
-                       $hits[] .= $this->resultWidget->render( $result, $terms, $offset++ );
-                       $result = $resultSet->next();
+               foreach ( $resultSet as $result ) {
+                       $hits[] = $this->resultWidget->render( $result, $terms, $offset++ );
                }
 
                return "<ul class='mw-search-results'>" . implode( '', $hits ) . "</ul>";
index d0c259f..248099a 100644 (file)
@@ -56,12 +56,10 @@ class SimpleSearchResultSetWidget implements SearchResultSetWidget {
 
                $iwResults = [];
                foreach ( $resultSets as $resultSet ) {
-                       $result = $resultSet->next();
-                       while ( $result ) {
+                       foreach ( $resultSet as $result ) {
                                if ( !$result->isBrokenTitle() ) {
                                        $iwResults[$result->getTitle()->getInterwiki()][] = $result;
                                }
-                               $result = $resultSet->next();
                        }
                }
 
index 321d5f8..1623c36 100644 (file)
@@ -2952,6 +2952,7 @@ class Language {
         * @deprecated No-op since 1.28
         */
        function initEncoding() {
+               wfDeprecated( __METHOD__, '1.28' );
                // No-op.
        }
 
@@ -2961,6 +2962,7 @@ class Language {
         * @deprecated No-op since 1.28
         */
        function recodeForEdit( $s ) {
+               wfDeprecated( __METHOD__, '1.28' );
                return $s;
        }
 
@@ -2970,6 +2972,7 @@ class Language {
         * @deprecated No-op since 1.28
         */
        function recodeInput( $s ) {
+               wfDeprecated( __METHOD__, '1.28' );
                return $s;
        }
 
@@ -3152,7 +3155,7 @@ class Language {
                        return;
                }
                $this->mMagicHookDone = true;
-               Hooks::run( 'LanguageGetMagic', [ &$this->mMagicExtensions, $this->getCode() ] );
+               Hooks::run( 'LanguageGetMagic', [ &$this->mMagicExtensions, $this->getCode() ], '1.16' );
        }
 
        /**
@@ -3208,7 +3211,7 @@ class Language {
                        $this->mExtendedSpecialPageAliases =
                                self::$dataCache->getItem( $this->mCode, 'specialPageAliases' );
                        Hooks::run( 'LanguageGetSpecialPageAliases',
-                               [ &$this->mExtendedSpecialPageAliases, $this->getCode() ] );
+                               [ &$this->mExtendedSpecialPageAliases, $this->getCode() ], '1.16' );
                }
 
                return $this->mExtendedSpecialPageAliases;
@@ -4321,13 +4324,18 @@ class Language {
         * the "raw" tag (-{R| }-) to prevent conversion.
         *
         * This function is called "markNoConversion" for historical
-        * reasons.
+        * reasons *BUT DIFFERS SIGNIFICANTLY* from
+        * LanguageConverter::markNoConversion(), with which it is easily
+        * confused.
         *
         * @param string $text Text to be used for external link
         * @param bool $noParse Wrap it without confirming it's a real URL first
         * @return string The tagged text
+        * @deprecated since 1.32, use LanguageConverter::markNoConversion()
+        *  instead.
         */
        public function markNoConversion( $text, $noParse = false ) {
+               wfDeprecated( __METHOD__, '1.32' );
                // Excluding protocal-relative URLs may avoid many false positives.
                if ( $noParse || preg_match( '/^(?:' . wfUrlProtocolsWithoutProtRel() . ')/', $text ) ) {
                        return $this->mConverter->markNoConversion( $text );
index e6a615e..6b67e8d 100644 (file)
        "retypenew": "Pasoë lom lageuëm barô:",
        "resetpass_submit": "Atô lageuëm rahsia lheuëh nyan tamöng",
        "changepassword-success": "Lageuëm rahsia droëneuh meuhasé geugantoë!",
+       "botpasswords-label-create": "Peugöt",
+       "botpasswords-label-update": "Peubarô",
+       "botpasswords-label-cancel": "Pubateue",
+       "botpasswords-label-delete": "Sampôh",
+       "botpasswords-label-resetpassword": "Atô keulayi lageuem rahsia",
        "resetpass_forbidden": "Lageuëm rahsia h'an jeuët geugantoë",
        "resetpass_forbidden-reason": "Lageuëm rahsia h`an jeuët geugantoë: $1",
        "resetpass-no-info": "Droëneuh suwah neutamöng mangat jeuët neu'eu laman nyoë",
        "interlanguage-link-title": "$1 – $2",
        "simpleantispam-label": "Paréksa anti-spam.\n<strong>BÈK</strong> neupasoë!",
        "pageinfo-title": "Keutrangan keu \"$1\"",
+       "pageinfo-header-basic": "Keutrangan peuneuphôn",
+       "pageinfo-header-restrictions": "Lindông mieng",
+       "pageinfo-display-title": "Judul tampilan",
+       "pageinfo-default-sort": "Gunci urôt baku",
+       "pageinfo-length": "Panyang mieng (lam bita)",
+       "pageinfo-article-id": "ID Mieng",
+       "pageinfo-language": "Bahsa asoe mieng",
+       "pageinfo-content-model": "Modèl asoe mieng",
+       "pageinfo-robot-policy": "Geuindèks lé robot",
        "pageinfo-watchers": "Jumeulah ureueng kalön mieng",
+       "pageinfo-redirects-name": "Jumeulah peuninah u mieng nyoe",
        "pageinfo-toolboxlink": "Keutrangan miëng",
        "previousdiff": "← Bida awai",
        "nextdiff": "Geunantoë lheuëh nyan →",
        "logentry-delete-delete": "$1 {{GENDER:$2|geusampôh}} miëng $3",
        "logentry-move-move": "$1 {{GENDER:$2|geupinah}} mieng $3 u $4",
        "logentry-move-move-noredirect": "$1 {{GENDER:$2|geupinah}} mieng $3 u $4 hana geubôh peuninah",
+       "logentry-patrol-patrol-auto": "$1 otomatis {{GENDER:$2|geutanda}} revisi $4 nibak mieng $3 nyang geukawai",
        "logentry-newusers-create": "$1 {{GENDER:$2|geupeugöt}} akun ureuëng ngui",
        "logentry-upload-upload": "$1 {{GENDER:$2|geupasoe}} $3",
        "searchsuggest-search": "Mita {{SITENAME}}",
index a8761fb..8f9b163 100644 (file)
        "resetpass-submit-loggedin": "تغيير كلمة السر",
        "resetpass-submit-cancel": "إلغاء",
        "resetpass-wrong-oldpass": "كلمة سر حالية أو مؤقتة غير صحيحة.\nربما تكون غيرت كلمة السر الخاصة بك بنجاح أو طلبت كلمة سر مؤقتة جديدة.",
-       "resetpass-recycled": "اÙ\84رجاء Ø¥Ø¹Ø§Ø¯Ø© ØªØ¹Ù\8aÙ\8aÙ\86 كلمة السر الخاصة بك إلى تركيبة أخرى غير كلمة السر الحالية.",
+       "resetpass-recycled": "اÙ\84رجاء ØªØºÙ\8aÙ\8aر كلمة السر الخاصة بك إلى تركيبة أخرى غير كلمة السر الحالية.",
        "resetpass-temp-emailed": "أنت مسجل الدخول حالياً بتركيبة مرسلة عبر البريد الإلكتروني. لإكمال عملية تسجيل الدخول‘ يجب إعادة تعيين كلمة السر هنا:",
        "resetpass-temp-password": "كلمة سر مؤقتة:",
        "resetpass-abort-generic": "منعت مُلحقة إتمام صيرورة تغيير كلمة السّر.",
        "resetpass-expired": "انتهت مدة صلاحية كلمة السر الخاصة بك. الرجاء تعيين كلمة سر جديدة لتسجيل الدخول.",
-       "resetpass-expired-soft": "انتهت مدة صلاحية كلمة السر الخاصة بك. الرجاء تعيين كلمة سر جديدة الآن أو النقر على زر إلغاء لإعادة تعيين كلمة السر لاحقاً.",
+       "resetpass-expired-soft": "انتهت مدة صلاحية كلمة السر الخاصة بك; الرجاء تغيير كلمة سر جديدة الآن أو النقر على زر إلغاء لإعادة تعيين كلمة السر لاحقاً.",
        "resetpass-validity-soft": "كلمة السر الخاصة بك غير صالحة :  $1 \n\nرجاءا اختر كلمة سر جديدة الآن، أو انقر فوق \"{{int:authprovider-resetpass-skip-label}}\" لتغييرها في وقت لاحق.",
        "passwordreset": "إعادة ضبط كلمة السر",
        "passwordreset-text-one": "أكمل هذا النموذج لإعادة ضبط كلمة السر الخاصة بك.",
        "rcfilters-other-review-tools": "أدوات مراجعة أخرى",
        "rcfilters-group-results-by-page": "جمع النتائج حسب الصفحة",
        "rcfilters-activefilters": "المرشحات النشطة",
+       "rcfilters-activefilters-hide": "إخفاء",
+       "rcfilters-activefilters-show": "عرض",
        "rcfilters-advancedfilters": "مرشحات متقدمة",
        "rcfilters-limit-title": "النتائج للعرض",
        "rcfilters-limit-and-date-label": "$1 {{PLURAL:$1|تغيير|تغييرات}}، $2",
        "rcfilters-savedqueries-rename": "أعد التسمية",
        "rcfilters-savedqueries-setdefault": "حفظ كقيمة افتراضية",
        "rcfilters-savedqueries-unsetdefault": "إزالة القيمة الافتراضية",
-       "rcfilters-savedqueries-remove": "أزÙ\84",
+       "rcfilters-savedqueries-remove": "حذÙ\81",
        "rcfilters-savedqueries-new-name-label": "الاسم",
        "rcfilters-savedqueries-new-name-placeholder": "صف الغرض من المرشح",
        "rcfilters-savedqueries-apply-label": "أنشئ مرشحا",
        "rcfilters-empty-filter": "لا مرشحات فعالة. كل المساهمات معروضة.",
        "rcfilters-filterlist-title": "مرشحات",
        "rcfilters-filterlist-whatsthis": "كيف تعمل هذه؟",
-       "rcfilters-filterlist-feedbacklink": "أخبرنا ما رأيك حول هذه المرشحات (الجديدة)",
+       "rcfilters-filterlist-feedbacklink": "أخبرنا ما رأيك حول هذه المرشحات",
        "rcfilters-highlightbutton-title": "التعليم على النتائج",
        "rcfilters-highlightmenu-title": "اختر لونًا",
        "rcfilters-highlightmenu-help": "اختر لونا للتعليم على هذه الخاصية",
index 823b09f..4f4aa01 100644 (file)
        "resetpass-submit-loggedin": "Camudar la contraseña",
        "resetpass-submit-cancel": "Encaboxar",
        "resetpass-wrong-oldpass": "Contraseña temporal o actual inválida.\nSeique yá camudaras la contraseña o que pidieras una nueva contraseña temporal.",
-       "resetpass-recycled": "Por favor, cambie la so contraseña por otra distinta de la actual.",
+       "resetpass-recycled": "Por favor, cambia la contraseña por otra distinta de la actual.",
        "resetpass-temp-emailed": "Anició sesión con un códigu temporal unviáu per corréu electrónicu.\nPa completar l'aniciu de sesión, tien de definir una nueva contraseña equí:",
        "resetpass-temp-password": "Contraseña temporal:",
        "resetpass-abort-generic": "Una estensión encaboxó'l cambiu de la contraseña.",
        "resetpass-expired": "La so contraseña caducó. Defina una nueva contraseña p'aniciar sesión.",
-       "resetpass-expired-soft": "La contraseña caducó y precisa reaniciase. Escueye agora una contraseña nueva, o pulsia «{{int:authprovider-resetpass-skip-label}}» pa reaniciala sero.",
-       "resetpass-validity-soft": "La contraseña nun ye válida: $1\n\nEscueye agora una contraseña nueva, o pulsia «{{int:authprovider-resetpass-skip-label}}» pa reaniciala sero.",
+       "resetpass-expired-soft": "La contraseña caducó y precisa cambiase. Escueye agora una contraseña nueva, o pulsia «{{int:authprovider-resetpass-skip-label}}» pa cambiala sero.",
+       "resetpass-validity-soft": "La contraseña nun ye válida: $1\n\nEscueye agora una contraseña nueva, o pulsia «{{int:authprovider-resetpass-skip-label}}» pa cambiala sero.",
        "passwordreset": "Reaniciar contraseña",
        "passwordreset-text-one": "Complete esti formulariu pa reaniciar la contraseña.",
        "passwordreset-text-many": "{{PLURAL:$1|Rellene unu de los campos pa recibir una contraseña temporal per corréu.}}",
        "rcfilters-other-review-tools": "Otres ferramientes de revisión",
        "rcfilters-group-results-by-page": "Agrupar resultancies per páxina",
        "rcfilters-activefilters": "Filtros activos",
+       "rcfilters-activefilters-hide": "Tapecer",
+       "rcfilters-activefilters-show": "Amosar",
        "rcfilters-advancedfilters": "Filtros avanzaos",
        "rcfilters-limit-title": "Resultancies qu'amosar",
        "rcfilters-limit-and-date-label": "$1 {{PLURAL:$1|cambiu|$1 cambios}}, $2",
        "rcfilters-empty-filter": "Nun hai filtros activos. Amuésense toles contribuciones.",
        "rcfilters-filterlist-title": "Filtros",
        "rcfilters-filterlist-whatsthis": "¿Como funciona esto?",
-       "rcfilters-filterlist-feedbacklink": "Dinos lo que pienses sobre estes (nueves) ferramientes de filtriáu",
+       "rcfilters-filterlist-feedbacklink": "Dinos lo que pienses sobre estes ferramientes de filtriáu",
        "rcfilters-highlightbutton-title": "Resaltar resultaos",
        "rcfilters-highlightmenu-title": "Seleiciona un color",
        "rcfilters-highlightmenu-help": "Seleiciona un color pa resaltar esta propiedá",
        "rcfilters-watchlist-showupdated": "Los cambeos fechos en páxines que nun visitasti desque se ficieron apaecen en <strong>negrina</strong>, con marcadores sólidos.",
        "rcfilters-preference-label": "Tapecer la versión meyorada de Cambios recién",
        "rcfilters-preference-help": "Revierte'l rediseñu de la interfaz de 2017 y toles ferramientes añadíes d'entós aquí.",
+       "rcfilters-watchlist-preference-label": "Tapecer la versión ameyorada de la Llista de siguimientu",
+       "rcfilters-watchlist-preference-help": "Desfai el rediseñu de la interfaz de 2017 y toles ferramientes añadíes d'entós acá.",
        "rcfilters-filter-showlinkedfrom-label": "Amosar los cambios nes páxines enllazaes dende",
        "rcfilters-filter-showlinkedfrom-option-label": "<strong>Páxines enllazaes dende</strong> la páxina seleicionada",
        "rcfilters-filter-showlinkedto-label": "Amosar los cambios nes páxines qu'enllacen a",
index a5bc6a4..a78003b 100644 (file)
        "resetpass-abort-generic": "Зьмяненьне паролю было скасаванае пашырэньнем.",
        "resetpass-expired": "Тэрмін дзеяньня вашага паролю скончыўся. Калі ласка, пазначце новы пароль для ўваходу ў сыстэму.",
        "resetpass-expired-soft": "Тэрмін дзеяньня вашага паролю скончыўся і ён патрабуе замены. Калі ласка, абярыце новы пароль цяпер або націсьніце «{{int:authprovider-resetpass-skip-label}}», каб зьмяніць яго пазьней.",
-       "resetpass-validity-soft": "Ваш пароль зьяўляецца некарэктным: $1\n\nКалі ласка, абярыце зараз новы пароль або націсьніце «{{int:authprovider-resetpass-skip-label}}», каб скінуць яго пазьней.",
+       "resetpass-validity-soft": "Ваш пароль зьяўляецца некарэктным: $1\n\nКалі ласка, абярыце цяпер новы пароль або націсьніце «{{int:authprovider-resetpass-skip-label}}», каб зьмяніць яго пазьней.",
        "passwordreset": "Ачыстка паролю",
        "passwordreset-text-one": "Запоўніце гэтую форму, каб атрымаць часовы пароль электроннай поштай.",
        "passwordreset-text-many": "{{PLURAL:$1|Запоўніце адно з палёў, каб атрымаць часовы пароль праз электронную пошту.}}",
        "rcfilters-other-review-tools": "Іншыя інструмэнты праверкі",
        "rcfilters-group-results-by-page": "Групаваць вынікі паводле старонак",
        "rcfilters-activefilters": "Актыўныя фільтры",
+       "rcfilters-activefilters-hide": "Схаваць",
+       "rcfilters-activefilters-show": "Паказаць",
        "rcfilters-advancedfilters": "Пашыраныя фільтры",
        "rcfilters-limit-title": "Паказаць вынікаў",
        "rcfilters-limit-and-date-label": "$1 {{PLURAL:$1|зьмена|зьмены|зьменаў}}, $2",
        "rcfilters-empty-filter": "Няма актыўных фільтраў. Паказаны ўвесь унёсак.",
        "rcfilters-filterlist-title": "Фільтры",
        "rcfilters-filterlist-whatsthis": "Як гэта працуе?",
-       "rcfilters-filterlist-feedbacklink": "Раскажыце нам, што вы думаеце пра гэтыя (новыя) фільтры",
+       "rcfilters-filterlist-feedbacklink": "Раскажыце нам, што вы думаеце пра гэтыя інструмэнты фільтраў",
        "rcfilters-highlightbutton-title": "Вылучыць вынікі",
        "rcfilters-highlightmenu-title": "Абярыце колер",
        "rcfilters-highlightmenu-help": "Абярыце колер для вылучэньня гэтай уласьцівасьці",
        "empty-file": "Дасланы вамі файл пусты.",
        "file-too-large": "Дасланы вамі файл занадта вялікі.",
        "filename-tooshort": "Назва файлу занадта кароткая.",
-       "filetype-banned": "Гэты тып файла забаронены.",
-       "verification-error": "Ð\93Ñ\8dÑ\82Ñ\8b Ñ\84айл Ð½Ðµ Ð¿Ñ\80айÑ\88оÑ\9e Ð²Ñ\8dÑ\80Ñ\8bÑ\84Ñ\96каÑ\86Ñ\8bÑ\8e.",
-       "hookaborted": "Ð\9fÑ\80апанаванаÑ\8f Ð\92амÑ\96 Ð·Ñ\8cмена Ð±Ñ\8bла Ð°Ð´Ñ\85Ñ\96ленаÑ\8f Ð°Ð¿Ñ\80аÑ\86оÑ\9eÑ\88Ñ\87Ñ\8bкам Ð¿Ð°Ñ\88Ñ\8bÑ\80Ñ\8dнÑ\8cнÑ\8f.",
-       "illegal-filename": "Недазволеная назва файла.",
-       "overwrite": "Замена існуючага файла забароненая.",
+       "filetype-banned": "Гэты тып файлу забаронены.",
+       "verification-error": "Ð\93Ñ\8dÑ\82Ñ\8b Ñ\84айл Ð½Ðµ Ð¿Ñ\80айÑ\88оÑ\9e Ð¿Ñ\80авеÑ\80кÑ\83.",
+       "hookaborted": "Ð\9fÑ\80апанаванаÑ\8f Ð²Ð°Ð¼Ñ\96 Ð·Ñ\8cмена Ð±Ñ\8bла Ð°Ð´Ñ\85Ñ\96ленаÑ\8f Ð¿Ð°Ñ\88Ñ\8bÑ\80Ñ\8dнÑ\8cнем.",
+       "illegal-filename": "Недазволеная назва файлу.",
+       "overwrite": "Замена існага файлу забароненая.",
        "unknown-error": "Узьнікла невядомая памылка.",
        "tmp-create-error": "Немагчыма стварыць часовы файл.",
        "tmp-write-error": "Памылка запісу часовага файла.",
index aaaf4ae..44f4040 100644 (file)
        "image_sample": "Напрыклад.jpg",
        "image_tip": "Файл у тэксце",
        "media_sample": "Напрыклад.ogg",
-       "media_tip": "Спасылка на медыя-файл",
+       "media_tip": "Спасылка на медыяфайл",
        "sig_tip": "Ваш подпіс і адзначаны час",
        "hr_tip": "Гарызантальная рыса (не злоўжывайце гэтым)",
        "summary": "Тлумачэнне:",
        "listduplicatedfiles": "Пералік дублікатных файлаў",
        "listduplicatedfiles-summary": "Гэта пералік файлаў, у якіх найноўшая версія файла ўяўляе сабою копію апошняй версіі якога-небудзь іншага файла. Улічваюцца толькі лакальныя файлы.",
        "listduplicatedfiles-entry": "[[:File:$1|$1]] мае [[$3|{{PLURAL:$2|дублікат|$2 дублікаты|$2 дублікатаў}}]].",
-       "unusedtemplates": "ШаблонÑ\8b, Ñ\8fкÑ\96Ñ\8f Ð½Ðµ Ð²Ñ\8bкаÑ\80Ñ\8bÑ\81Ñ\82аны",
+       "unusedtemplates": "Ð\9dевÑ\8bкаÑ\80Ñ\8bÑ\81Ñ\82анÑ\8bÑ\8f Ñ\88аблоны",
        "unusedtemplatestext": "Тут пералічаныя ўсе старонкі ў прасторы назваў {{ns:template}}, якія не ўкладзеныя ў іншыя старонкі.\nПамятайце, што трэба правяраць спасылкі на шаблоны перад тым, як выдаляць іх.",
        "unusedtemplateswlh": "іншыя спасылкі",
        "randompage": "Выпадковая старонка",
index 7e76d0e..503f7ec 100644 (file)
        "expansion-depth-exceeded-warning": "পাতাটি এক্সেপশন সীমানা অতিক্রম করেছে",
        "parser-unstrip-loop-warning": "ত্রুটিপূর্ণ লুপ খুঁজে পাওয়া গিয়েছে",
        "unstrip-depth-warning": "লুপ রিকারশন সীমানা অতিক্রম করেছে ($1)",
+       "unstrip-depth-category": "পাতাগুলি যেখানে unstrip গভীরতার সীমা অতিক্রম করেছেন",
+       "unstrip-size-warning": "Unstrip আকারের সীমা অতিক্রম করেছে ($1)",
        "converter-manual-rule-error": "ম্যানুয়াল ভাষা রূপান্তর নিয়মে ত্রুটি পাওয়া গিয়েছে",
        "undo-success": "সম্পাদনাটি বাতিল করা যাবে। অনুগ্রহ করে নিচের তুলনাটি পরীক্ষা করে দেখুন ও নিশ্চিত করুন যে এটাই আপনি করতে চান, এবং তারপর নিচের সম্পাদনাগুলি সংরক্ষণ করে সম্পাদনাটির বাতিল প্রক্রিয়া সমাপ্ত করুন।",
        "undo-failure": "এ সম্পাদনা মধ্যবর্তী সম্পাদনাসমূহের কারণে পূর্বাবস্থায় ফিরিয়ে নেওয়া যাবে না।",
        "rcfilters-other-review-tools": "অন্যান্য পর্যালোচনা সরঞ্জাম",
        "rcfilters-group-results-by-page": "পাতা অনুযায়ী দলের ফলাফল",
        "rcfilters-activefilters": "সক্রিয় ছাঁকনিসমূহ",
+       "rcfilters-activefilters-hide": "লুকান",
+       "rcfilters-activefilters-show": "দেখান",
        "rcfilters-advancedfilters": "উন্নত ছাঁকনি",
        "rcfilters-limit-title": "যেসব ফলাফল দেখাবে",
        "rcfilters-limit-and-date-label": "$1টি {{PLURAL:$1|পরিবর্তন}}, $2",
        "rcfilters-savedqueries-rename": "নামান্তর",
        "rcfilters-savedqueries-setdefault": "পূর্ব-নির্ধারিত হিসেবে নির্ধারন করুন",
        "rcfilters-savedqueries-unsetdefault": "পূর্ব-নির্ধারিত হিসেবে নির্ধারন সরান",
-       "rcfilters-savedqueries-remove": "সরান",
+       "rcfilters-savedqueries-remove": "à¦\85পসারণ",
        "rcfilters-savedqueries-new-name-label": "নাম",
        "rcfilters-savedqueries-new-name-placeholder": "ছাঁকনির উদ্দেশ্য বর্ণনা করুন",
        "rcfilters-savedqueries-apply-label": "ছাঁকনি তৈরি করুন",
        "rcfilters-empty-filter": "কোনো সক্রিয় ফিল্টার নেই। সমস্ত অবদান দেখানো হয়েছে।",
        "rcfilters-filterlist-title": "ছাঁকনি",
        "rcfilters-filterlist-whatsthis": "এগুলি কিভাবে কাজ করে?",
-       "rcfilters-filterlist-feedbacklink": "আপনি এই (নতুন) ছাঁকন সরঞ্জাম সম্পর্কে কি মনে করেন তা আমাদের বলুন",
+       "rcfilters-filterlist-feedbacklink": "আপনি এই ছাঁকন সরঞ্জাম সম্পর্কে কি মনে করেন তা আমাদের বলুন",
        "rcfilters-highlightbutton-title": "ফলাফলে আলোকপাত করুন",
        "rcfilters-highlightmenu-title": "একটি রং নির্বাচন করুন",
        "rcfilters-highlightmenu-help": "এই বৈশিষ্ট্যটিতে আলোকপাত করতে একটি রঙ নির্বাচন করুন",
        "protectedtitles-submit": "শিরোনাম প্রদর্শন করুন",
        "listusers": "ব্যবহারকারীর তালিকা",
        "listusers-editsonly": "শুধুমাত্র এমন ব্যবহারকারীদের দেখাও যাদের অবদান আছে",
+       "listusers-temporarygroupsonly": "শুধুমাত্র অস্থায়ী ব্যবহারকারী দলের ব্যবহারকারীদের দেখান",
        "listusers-creationsort": "তৈরির তারিখ অনুসারে সাজাও",
        "listusers-desc": "বড় থেকে ছোট ক্রম অনুযায়ী সাজাও",
        "usereditcount": "$1 {{PLURAL:$1|সম্পাদনা|সম্পাদনা}}",
index 073b2ce..d93159b 100644 (file)
        "savechanges": "Enrollañ ar c'hemmoù",
        "publishpage": "Embann ar bajenn",
        "publishchanges": "Embann ar c'hemmoù",
+       "savearticle-start": "Enrollañ ar bajenn...",
+       "savechanges-start": "Enrollañ ar c'hemmoù...",
+       "publishpage-start": "Embann ar bajenn...",
+       "publishchanges-start": "Embann ar c'hemmoù",
        "preview": "Rakwelet",
        "showpreview": "Rakwelet",
        "showdiff": "Diskouez ar c'hemmoù",
        "prefs-dateformat": "Furmad an deiziadoù",
        "prefs-timeoffset": "Linkadur eur",
        "prefs-advancedediting": "Dibarzhioù araokaet",
+       "prefs-developertools": "Ostilhoù diorren",
        "prefs-editor": "Aozer",
        "prefs-preview": "Rakwelet",
        "prefs-advancedrc": "Dibarzhioù araokaet",
+       "prefs-opt-out": "Nac'hañ ar c'hemmoù",
        "prefs-advancedrendering": "Dibarzhioù araokaet",
        "prefs-advancedsearchoptions": "Dibarzhioù araokaet",
        "prefs-advancedwatchlist": "Dibarzhioù araokaet",
        "prefs-diffs": "Diforc'hioù",
        "prefs-help-prefershttps": "Efediñ a ray an dibarzh-mañ kentañ gwech ma kevreoc'h.",
        "prefswarning-warning": "Kemmet eo bet ho penndibaboù ganeoc'h, met enrollet n'int ket bet avat.\nMar kuitait ar bajenn-mañ hep klikañ war \"$1\" ne vo ket nevesaet ho penndibaboù",
-       "prefs-tabs-navigation-hint": "Titourig : Gallout a rit implijout an touchennoù bir kleiz ha bir dehoù evit merdeiñ etre an ivinelloù e roll an ivinelloù.",
+       "prefs-tabs-navigation-hint": "Titourig : Gallout a rit implijout an touchennoù bir kleiz ha bir dehou evit merdeiñ etre an ivinelloù e roll an ivinelloù.",
        "userrights": "Gwirioù an implijer",
        "userrights-lookup-user": "Diuzañ un implijer",
        "userrights-user-editname": "Lakait un anv implijer :",
        "apisandbox-dynamic-error-exists": "Bez' ez eus c'hoazh eus un arventenn anvet \"$1\".",
        "apisandbox-deprecated-parameters": "Arventennoù dispredet",
        "apisandbox-fetch-token": "Leuniañ emgefre ar jedouer",
+       "apisandbox-add-multi": "Ouzhpennañ",
        "apisandbox-submit-invalid-fields-title": "Direizh eo maeziennoù zo",
        "apisandbox-submit-invalid-fields-message": "Reizhit ar maeziennoù merket ha klaskit en-dro.",
        "apisandbox-results": "Disoc'hoù",
        "newimages-user": "Chomlec'h IP pe anv implijer",
        "newimages-showbots": "Diskouez an ezporzhiadennoù graet gant robotoù",
        "newimages-hidepatrolled": "Kuzhat ar enporzhiadennoù gwiriet",
+       "newimages-mediatype": "Doare media :",
        "noimages": "Netra da welet.",
        "gallery-slideshow-toggle": "Gwintañ ar munudoù",
        "ilsubmit": "Klask",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Dikedenn|Tikedenn}}]] : $2)",
        "tag-mw-contentmodelchange": "cheñch ar patrom danvez",
        "tag-mw-contentmodelchange-description": "KEmmoù a [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:ChangeContentModel cheñch patrom danvez] ur bajenn",
+       "tag-mw-new-redirect": "Adkas nevez",
        "tag-mw-undo": "Dizober",
        "tags-title": "Tikedennoù",
        "tags-intro": "Rollañ a ra ar bajenn-mañ an tikedennoù a c'hall ar meziant implijout da verkañ kemmoù hag o zalvoudegezh.",
        "undelete-cantedit": "N'hallit ket diziverkañ ar bajenn-mañ rak n'oc'h ket aotreet da gemmañ anezhi.",
        "undelete-cantcreate": "N'hallit ket diziverkañ ar bajenn-mañ rak n'eus pajenn ebet gant an anv-mañ ha n'oc'h ket aotreet da grouiñ ar bajenn-mañ.",
        "pagedata-title": "Roadennoù ar bajenn",
-       "pagedata-bad-title": "Titl direizh : $1."
+       "pagedata-bad-title": "Titl direizh : $1.",
+       "passwordpolicies-group": "Strollad",
+       "passwordpolicies-policies": "Politikerezh"
 }
index 3c31bf0..6800d66 100644 (file)
        "moredotdotdot": "Lainna...",
        "morenotlisted": "Daftar on mungkin inda singkop",
        "mypage": "Alaman",
-       "mytalk": "Dokon",
+       "mytalk": "Obar",
        "anontalk": "Obar",
        "navigation": "Navigasi",
        "and": "&#32;dot",
        "create-local": "Baen deskripsi lokal",
        "delete": "Apus",
        "newpage": "Alaman baru",
-       "talkpagelinktext": "Dokon",
+       "talkpagelinktext": "obar",
        "personaltools": "Alat pribadi",
        "talk": "Marpokat",
        "views": "Sise",
        "rcshowhidemine-show": "Patidaon",
        "rcshowhidemine-hide": "Bunion",
        "rclinks": "Patidaon sude $1 parubaan $2 ari",
-       "diff": "diff",
-       "hist": "hist",
+       "diff": "beda",
+       "hist": "versi",
        "hide": "Bunion",
        "show": "Alaman pamake",
        "minoreditletter": "m",
        "listgrouprights-members": "(daftar anggota)",
        "emailuser": "Email ni pamake on",
        "usermessage-editor": "Tona ni sistem",
-       "watchlist": "Pamataan",
-       "mywatchlist": "Pamatai",
+       "watchlist": "Daftar Pamataan",
+       "mywatchlist": "Daftar pamataan",
        "watchlistfor2": "Tu $1 $2",
        "watch": "Pamatai",
        "unwatch": "Inda ipamatai",
        "sp-contributions-blocklog": "Log blokir",
        "sp-contributions-uploads": "Unggah",
        "sp-contributions-logs": "Log",
-       "sp-contributions-talk": "Dokon",
+       "sp-contributions-talk": "obar",
        "sp-contributions-search": "Jalaki kontribusi",
        "sp-contributions-username": "Alama IP Pangguna",
        "sp-contributions-toponly": "Umna patidaon editan revisi parpudi",
index fdadfc5..5dac858 100644 (file)
        "resetpass-submit-loggedin": "Canvia la contrasenya",
        "resetpass-submit-cancel": "Cancel·la",
        "resetpass-wrong-oldpass": "Contrasenya actual o temporal no vàlida.\nDeveu haver canviat la vostra contrasenya o demanat una nova contrasenya temporal.",
-       "resetpass-recycled": "Restabliu la contrasenya amb un text diferent que el de la contrasenya actual.",
+       "resetpass-recycled": "Canvieu la contrasenya amb un text diferent que el de la contrasenya actual.",
        "resetpass-temp-emailed": "Heu iniciat una sessió amb un codi temporal enviat per correu.\nPer completar l'inici de sessió heu de definir una contrasenya nova a continuació:",
        "resetpass-temp-password": "Contrasenya temporal:",
        "resetpass-abort-generic": "Una extensió ha interromput el canvi de contrasenya.",
        "resetpass-expired": "La contrasenya ha vençut. Definiu una contrasenya nova per iniciar la sessió.",
-       "resetpass-expired-soft": "La contrasenya ha vençut i cal restablir-la. Trieu una contrasenya nova ara, o feu clic a «{{int:authprovider-resetpass-skip-label}}» per a restablir-la més endavant.",
-       "resetpass-validity-soft": "La contrasenya no és vàlida: $1\n\nTrieu una nova contrasenya ara o cliqueu \"{{int:authprovider-resetpass-skip-label}}\" per a restablir-la més endavant.",
+       "resetpass-expired-soft": "La contrasenya ha vençut i cal canviar-la. Trieu una contrasenya nova ara, o feu clic a «{{int:authprovider-resetpass-skip-label}}» per a canviar-la més endavant.",
+       "resetpass-validity-soft": "La contrasenya no és vàlida: $1\n\nTrieu una nova contrasenya ara o cliqueu \"{{int:authprovider-resetpass-skip-label}}\" per a canviar-la més endavant.",
        "passwordreset": "Restablir contrasenya",
        "passwordreset-text-one": "Cal completar aquest formulari per reiniciar la contrasenya",
        "passwordreset-text-many": "{{PLURAL:$1|Ompliu un dels camps per a rebre una contrasenya temporal al vostre correu electrònic.}}",
        "rcfilters-other-review-tools": "Altres eines de supervisió",
        "rcfilters-group-results-by-page": "Agrupa els resultats per pàgina",
        "rcfilters-activefilters": "Filtres actius",
+       "rcfilters-activefilters-hide": "Amaga",
+       "rcfilters-activefilters-show": "Mostra",
        "rcfilters-advancedfilters": "Filtres avançats",
        "rcfilters-limit-title": "Resultats a mostrar",
        "rcfilters-limit-and-date-label": "$1 {{PLURAL:$1|canvi|canvis}}, $2",
        "rcfilters-empty-filter": "No hi ha cap filtre actiu. Es mostren totes les contribucions.",
        "rcfilters-filterlist-title": "Filtres",
        "rcfilters-filterlist-whatsthis": "Com funciona això?",
-       "rcfilters-filterlist-feedbacklink": "Comenta'ns què et semblen aquestes (noves) eines per filtrar",
+       "rcfilters-filterlist-feedbacklink": "Comenteu-nos què us semblen aquestes eines de filtratge",
        "rcfilters-highlightbutton-title": "Ressalta els resultats",
        "rcfilters-highlightmenu-title": "Selecciona un color",
        "rcfilters-highlightmenu-help": "Seleccioneu un color per ressaltar la propietat",
        "rcfilters-filtergroup-reviewstatus": "Estat de revisió",
        "rcfilters-filter-reviewstatus-unpatrolled-label": "No patrullat",
        "rcfilters-filter-reviewstatus-manual-label": "Patrullat manualment",
+       "rcfilters-filter-reviewstatus-auto-label": "Autopatrullat",
        "rcfilters-filtergroup-significance": "Significació",
        "rcfilters-filter-minor-label": "Modificacions menors",
        "rcfilters-filter-minor-description": "Modificacions que l'autor va etiquetar com a menors.",
        "rcfilters-watchlist-edit-watchlist-button": "Editeu la vostra llista de pàgines seguides",
        "rcfilters-watchlist-showupdated": "Els canvis fets en pàgines que no heu visitat des que s'efectuaren apareixen en <strong>negreta</strong> amb un punt sòlid al costat.",
        "rcfilters-preference-label": "Amaga la versió millorada de Canvis recents",
+       "rcfilters-filter-showlinkedfrom-label": "Mostra els canvis en les pàgines enllaçades des de",
+       "rcfilters-filter-showlinkedfrom-option-label": "<strong>Pàgines enllaçades des de</strong> la pàgina seleccionada",
        "rcfilters-filter-showlinkedto-label": "Mostra els canvis a les pàgines que enllacin a",
        "rcfilters-filter-showlinkedto-option-label": "<strong>Pàgines que enllacen a</strong> la pàgina seleccionada",
        "rcfilters-target-page-placeholder": "Escriviu el nom d’una pàgina (o d’una categoria)",
        "dellogpage": "Registre de supressions",
        "dellogpagetext": "Davall hi ha una llista dels esborraments més recents.",
        "deletionlog": "registre de supressions",
+       "log-name-create": "Registre de creació de pàgines",
        "reverted": "Invertit amb una revisió anterior",
        "deletecomment": "Motiu:",
        "deleteotherreason": "Motiu diferent o addicional:",
        "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Etiqueta|Etiquetes}}]]: $2)",
        "tag-mw-contentmodelchange": "canvi de model de contingut",
        "tag-mw-new-redirect": "Redirecció nova",
+       "tag-mw-removed-redirect": "Redirecció suprimida",
+       "tag-mw-changed-redirect-target": "La destinació de la redirecció ha canviat",
        "tag-mw-blank": "Buidament",
+       "tag-mw-blank-description": "Modificacions que blanquegen una pàgina",
        "tag-mw-replace": "Substitució",
+       "tag-mw-undo": "Desfés",
        "tags-title": "Etiquetes",
        "tags-intro": "Aquesta pàgina llista les etiquetes amb què el programari pot marcar una modificació, i el seu significat.",
        "tags-tag": "Nom de l'etiqueta",
        "compare-title-not-exists": "El títol que heu especificat no existeix.",
        "compare-revision-not-exists": "La revisió que heu especificat no existeix.",
        "diff-form": "Diferències",
+       "diff-form-oldid": "ID de la revisió antiga (opcional)",
        "diff-form-submit": "Mostra les diferències",
        "permanentlink": "Enllaç permanent",
        "permanentlink-revid": "ID de la revisó",
index b703394..0f2f3fc 100644 (file)
        "dellogpage": "ДӀадаьхнарш долу тéптар",
        "dellogpagetext": "Лахахь гойтуш ю тӀаьххьара дӀаяьхнарш.",
        "deletionlog": "дӀадаьхнарш долу тéптар",
+       "log-name-create": "АгӀонаш кхолларан тептар",
+       "log-description-create": "Лахахь гойтуш ю тӀаьххьара кхоьллина агӀонаш.",
+       "logentry-create-create": "$1 {{GENDER:$2|кхоьллина}} $3 агӀо",
        "deletecomment": "Бахьана:",
        "deleteotherreason": "Кхин бахьана/тӀетохар:",
        "deletereasonotherlist": "Кхин бахьана",
index 8cb2b8f..ef417f5 100644 (file)
        "special-characters-group-khmer": "خمێری",
        "mw-widgets-usersmultiselect-placeholder": "زیادکردن...",
        "log-action-filter-block": "جۆری بلۆک:",
+       "log-action-filter-contentmodel": "جۆری گۆڕینی مۆدێلی ناوەڕۆک:",
        "log-action-filter-all": "ھەموو",
        "log-action-filter-upload-upload": "بارکردنی نوێ",
        "authmanager-email-label": "ئیمەیڵ"
index 788a416..4997046 100644 (file)
        "rcfilters-other-review-tools": "Další kontrolní nástroje",
        "rcfilters-group-results-by-page": "Seskupit výsledky podle stránky",
        "rcfilters-activefilters": "Aktivní filtry",
+       "rcfilters-activefilters-hide": "Skrýt",
+       "rcfilters-activefilters-show": "Zobrazit",
        "rcfilters-advancedfilters": "Pokročilé filtry",
        "rcfilters-limit-title": "Zobrazené výsledky",
        "rcfilters-limit-and-date-label": "{{PLURAL:$1|Jedna změna|$1 změny|$1 změn}}, $2",
        "rcfilters-savedqueries-rename": "Přejmenovat",
        "rcfilters-savedqueries-setdefault": "Nastavit jako výchozí",
        "rcfilters-savedqueries-unsetdefault": "Nemít jako výchozí",
-       "rcfilters-savedqueries-remove": "Odstranit",
+       "rcfilters-savedqueries-remove": "Smazat",
        "rcfilters-savedqueries-new-name-label": "Název",
        "rcfilters-savedqueries-new-name-placeholder": "Popište účel filtru",
        "rcfilters-savedqueries-apply-label": "Vytvořit filtr",
        "rcfilters-empty-filter": "Žádné aktivní filtry. Zobrazeny jsou všechny příspěvky.",
        "rcfilters-filterlist-title": "Filtry",
        "rcfilters-filterlist-whatsthis": "Jak to funguje?",
-       "rcfilters-filterlist-feedbacklink": "Řekněte nám, co si myslíte o těchto (nových) filtrech",
+       "rcfilters-filterlist-feedbacklink": "Sdělte nám svůj názor na tyto filtrovací nástroje",
        "rcfilters-highlightbutton-title": "Zvýraznit výsledky",
        "rcfilters-highlightmenu-title": "Vybrat barvu",
        "rcfilters-highlightmenu-help": "Vyberte barvu pro zvýraznění této vlastnosti",
        "rcfilters-watchlist-showupdated": "Změny stránek, které jste od provedení změn nenavštívili, jsou zobrazeny <strong>tučně</strong> s vyplněnou značkou.",
        "rcfilters-preference-label": "Skrýt vylepšenou verzi posledních změn",
        "rcfilters-preference-help": "Zruší novou podobu rozhraní zavedenou v roce 2017 a všechny nástroje přidané od té doby.",
+       "rcfilters-watchlist-preference-label": "Skrýt vylepšenou verzi sledovaných stránek",
+       "rcfilters-watchlist-preference-help": "Zruší novou podobu rozhraní zavedenou v roce 2017 a všechny nástroje přidané od té doby.",
        "rcfilters-filter-showlinkedfrom-label": "Zobrazit změny stránek, na které se odkazuje",
        "rcfilters-filter-showlinkedfrom-option-label": "<strong>Stránky odkazované z</strong> vybrané stránky",
        "rcfilters-filter-showlinkedto-label": "Zobrazit změny stránek, které sem odkazují",
        "whatlinkshere": "Odkazuje sem",
        "whatlinkshere-title": "Stránky odkazující na „$1“",
        "whatlinkshere-page": "Strana:",
-       "linkshere": "Na <strong>$1</strong> odkazují tyto stránky:",
-       "nolinkshere": "Žádná stránka na <strong>$1</strong> neodkazuje.",
-       "nolinkshere-ns": "Ve zvoleném jmenném prostoru na <strong>$1</strong> neodkazuje žádná stránka.",
+       "linkshere": "Na <strong>$2</strong> odkazují tyto stránky:",
+       "nolinkshere": "Žádná stránka na <strong>$2</strong> neodkazuje.",
+       "nolinkshere-ns": "Ve zvoleném jmenném prostoru na <strong>$2</strong> neodkazuje žádná stránka.",
        "isredirect": "přesměrování",
        "istemplate": "vložení",
        "isimage": "vložení souboru",
index 35a0f30..3a3478c 100644 (file)
        "specialpages-group-media": "Медиа-материалсемпе тултарăшсем",
        "specialpages-group-users": "Хутшăнакансем тата правасем",
        "specialpages-group-highuse": "Нумай усă куракан страницăсем",
-       "tag-list-wrapper": "([[Special:Tags|$1 метка]]: $2)",
+       "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Тег|Тегсем}}]]: $2)",
+       "tags-title": "Тегсем",
        "compare-submit": "Танлаштар",
        "htmlform-selectorother-other": "Урăххи",
        "htmlform-no": "Çук",
index 1936b0e..cab15ae 100644 (file)
        "resetpass-submit-loggedin": "Passwort ändern",
        "resetpass-submit-cancel": "Abbrechen",
        "resetpass-wrong-oldpass": "Ungültiges temporäres oder aktuelles Passwort.\nMöglicherweise hast du dein Passwort bereits geändert oder ein neues temporäres Passwort beantragt.",
-       "resetpass-recycled": "Bitte setze dein Passwort auf ein anderes Passwort als dein aktuelles fest.",
+       "resetpass-recycled": "Bitte ändere dein Passwort auf ein anderes als dein aktuelles.",
        "resetpass-temp-emailed": "Du hast dich mit einem temporären E-Mail-Code angemeldet.\nUm die Anmeldung abzuschließen, musst du jetzt ein neues Passwort festlegen:",
        "resetpass-temp-password": "Temporäres Passwort:",
        "resetpass-abort-generic": "Die Passwortänderung wurde durch eine Erweiterung abgebrochen.",
        "resetpass-expired": "Dein Passwort ist abgelaufen. Bitte lege ein neues Passwort zur Anmeldung fest.",
-       "resetpass-expired-soft": "Dein Passwort ist abgelaufen und muss zurückgesetzt werden. Bitte wähle jetzt ein neues Passwort aus oder klicke auf „{{int:authprovider-resetpass-skip-label}}“, um es später zurückzusetzen.",
-       "resetpass-validity-soft": "Dein Passwort ist ungültig: $1\n\nBitte wähle jetzt ein neues Passwort oder klicke auf „{{int:authprovider-resetpass-skip-label}}“, um es später zurückzusetzen.",
+       "resetpass-expired-soft": "Dein Passwort ist abgelaufen und muss geändert werden. Bitte wähle jetzt ein neues Passwort aus oder klicke auf „{{int:authprovider-resetpass-skip-label}}“, um es später zu ändern.",
+       "resetpass-validity-soft": "Dein Passwort ist ungültig: $1\n\nBitte wähle jetzt ein neues Passwort aus oder klicke auf „{{int:authprovider-resetpass-skip-label}}“, um es später zu ändern.",
        "passwordreset": "Passwort zurücksetzen",
        "passwordreset-text-one": "Fülle dieses Formular aus, um ein temporäres Passwort per E-Mail zu erhalten.",
        "passwordreset-text-many": "{{PLURAL:$1|Fülle eines der Felder aus, um ein temporäres Passwort per E-Mail zugesandt zu bekommen.}}",
        "rcfilters-other-review-tools": "Andere Überprüfungswerkzeuge",
        "rcfilters-group-results-by-page": "Ergebnisse nach Seite gruppieren",
        "rcfilters-activefilters": "Aktive Filter",
+       "rcfilters-activefilters-hide": "Ausblenden",
+       "rcfilters-activefilters-show": "Anzeigen",
        "rcfilters-advancedfilters": "Erweiterte Filter",
        "rcfilters-limit-title": "Anzuzeigende Ergebnisse",
        "rcfilters-limit-and-date-label": "{{PLURAL:$1|Eine Änderung|$1 Änderungen}}, $2",
        "rcfilters-savedqueries-rename": "Umbenennen",
        "rcfilters-savedqueries-setdefault": "Als Standard festlegen",
        "rcfilters-savedqueries-unsetdefault": "Als Standard entfernen",
-       "rcfilters-savedqueries-remove": "Entfernen",
+       "rcfilters-savedqueries-remove": "Löschen",
        "rcfilters-savedqueries-new-name-label": "Name",
        "rcfilters-savedqueries-new-name-placeholder": "Beschreibe den Zweck des Filters",
        "rcfilters-savedqueries-apply-label": "Filter erstellen",
        "rcfilters-empty-filter": "Keine aktiven Filter. Es werden alle Beiträge angezeigt.",
        "rcfilters-filterlist-title": "Filter",
        "rcfilters-filterlist-whatsthis": "Wie funktioniert das?",
-       "rcfilters-filterlist-feedbacklink": "Erzähle uns, was du über diese (neuen) Filterwerkzeuge denkst.",
+       "rcfilters-filterlist-feedbacklink": "Erzähle uns, was du über diese Filterwerkzeuge denkst.",
        "rcfilters-highlightbutton-title": "Ergebnisse hervorheben",
        "rcfilters-highlightmenu-title": "Eine Farbe auswählen",
        "rcfilters-highlightmenu-help": "Eine Farbe auswählen, um diese Eigenschaft hervorzuheben.",
index 5265f3a..152ca60 100644 (file)
        "confirm-unwatch-top": "Κατάργηση αυτής της σελίδας από τη λίστα παρακολούθησης σας;",
        "confirm-rollback-button": "Εντάξει",
        "confirm-rollback-top": "Επαναφέρετε τις επεξεργασίες σε αυτή τη σελίδα;",
+       "semicolon-separator": ",&#32,",
        "quotation-marks": "«$1»",
        "imgmultipageprev": "← προηγούμενη σελίδα",
        "imgmultipagenext": "επόμενη σελίδα →",
index fcf2e25..86a05f0 100644 (file)
        "resetpass-submit-loggedin": "Change password",
        "resetpass-submit-cancel": "Cancel",
        "resetpass-wrong-oldpass": "Invalid temporary or current password.\nYou may have already changed your password or requested a new temporary password.",
-       "resetpass-recycled": "Please reset your password to something other than your current password.",
+       "resetpass-recycled": "Please change your password to something other than your current password.",
        "resetpass-temp-emailed": "You logged in with a temporary emailed code.\nTo finish logging in, you must set a new password here:",
        "resetpass-temp-password": "Temporary password:",
        "resetpass-abort-generic": "Password change has been aborted by an extension.",
        "resetpass-expired": "Your password has expired. Please set a new password to log in.",
-       "resetpass-expired-soft": "Your password has expired and needs to be reset. Please choose a new password now, or click \"{{int:authprovider-resetpass-skip-label}}\" to reset it later.",
-       "resetpass-validity-soft": "Your password is not valid: $1\n\nPlease choose a new password now, or click \"{{int:authprovider-resetpass-skip-label}}\" to reset it later.",
+       "resetpass-expired-soft": "Your password has expired and needs to be changed. Please choose a new password now, or click \"{{int:authprovider-resetpass-skip-label}}\" to change it later.",
+       "resetpass-validity-soft": "Your password is not valid: $1\n\nPlease choose a new password now, or click \"{{int:authprovider-resetpass-skip-label}}\" to change it later.",
        "passwordreset": "Reset password",
        "passwordreset-text-one": "Complete this form to receive a temporary password via email.",
        "passwordreset-text-many": "{{PLURAL:$1|Fill in one of the fields to receive a temporary password via email.}}",
        "rcfilters-other-review-tools": "Other review tools",
        "rcfilters-group-results-by-page": "Group results by page",
        "rcfilters-activefilters": "Active filters",
+       "rcfilters-activefilters-hide": "Hide",
+       "rcfilters-activefilters-show": "Show",
        "rcfilters-advancedfilters": "Advanced filters",
        "rcfilters-limit-title": "Results to show",
        "rcfilters-limit-and-date-label": "$1 {{PLURAL:$1|change|changes}}, $2",
        "rcfilters-savedqueries-rename": "Rename",
        "rcfilters-savedqueries-setdefault": "Set as default",
        "rcfilters-savedqueries-unsetdefault": "Remove as default",
-       "rcfilters-savedqueries-remove": "Remove",
+       "rcfilters-savedqueries-remove": "Delete",
        "rcfilters-savedqueries-new-name-label": "Name",
        "rcfilters-savedqueries-new-name-placeholder": "Describe the purpose of the filter",
        "rcfilters-savedqueries-apply-label": "Create filter",
        "rcfilters-empty-filter": "No active filters. All contributions are shown.",
        "rcfilters-filterlist-title": "Filters",
        "rcfilters-filterlist-whatsthis": "How do these work?",
-       "rcfilters-filterlist-feedbacklink": "Tell us what you think about these (new) filtering tools",
+       "rcfilters-filterlist-feedbacklink": "Tell us what you think about these filtering tools",
        "rcfilters-highlightbutton-title": "Highlight results",
        "rcfilters-highlightmenu-title": "Select a color",
        "rcfilters-highlightmenu-help": "Select a color to highlight this property",
index c7019d8..bf4dc22 100644 (file)
        "resetpass-temp-password": "Provizora pasvorto:",
        "resetpass-abort-generic": "Ŝanĝo de pasvorto estis nuligita per kromprogramo.",
        "resetpass-expired": "Via pasvorto eksvalidiĝis. Bonvolu difini novan pasvorton por la alsalutado.",
-       "resetpass-expired-soft": "Via pasvorto eksvalidiĝis kaj devas esti rekomencigata. Bonvolu nun elekti novan pasvorton, aŭ klaki \"{{int:authprovider-resetpass-skip-label}}\" por rekomencigi ĝin pli malfrue.",
+       "resetpass-expired-soft": "Via pasvorto eksvalidiĝis kaj devas esti ŝanĝata. Bonvolu nun elekti novan pasvorton, aŭ klaki \"{{int:authprovider-resetpass-skip-label}}\" por ŝanĝi ĝin pli malfrue.",
        "resetpass-validity-soft": "Via pasvorto ne estas valida: $1\n\nBonvolu elekti novan pasvorton nun, aŭ klaku \"{{int:authprovider-resetpass-skip-label}}\", por rekomencigi ĝin pli malfrue.",
        "passwordreset": "Restarigo de pasvorto",
        "passwordreset-text-one": "Plenigu ĉi tiun formularon por renovigi vian pasvorton.",
        "revdelete-edit-reasonlist": "Redakti kialojn por forigo",
        "revdelete-offender": "Aŭtoro de revizio:",
        "suppressionlog": "Protokolo pri subigado",
-       "suppressionlogtext": "Malsupre estas listo de forigoj kaj forbaroj pri enhavo kaŝita de administrantoj.\nRigardu la [[Special:BlockList|forbarliston]] por la listo de nune operaciaj forbaroj kaj forigoj.",
+       "suppressionlogtext": "Jen listo de forigoj kaj forbaroj pri enhavo kaŝita per administrantoj.\nVidi la [[Special:BlockList|forbarliston]] por la listo de forigoj kaj forbaroj aktuale operaciaj.",
        "mergehistory": "Kunigi historiojn de paĝoj",
        "mergehistory-header": "Ĉi tiu paĝo permesas al vi kunigi versiojn de la historio de unu fonta paĝo en pli novan paĝon.\nCertigu ke ĉi tiu ŝanĝo tenos kontinuecon de la historia paĝo.",
        "mergehistory-box": "Kunigi versiojn de du paĝoj:",
index 376baca..cc44a70 100644 (file)
        "resetpass-submit-loggedin": "Cambiar contraseña",
        "resetpass-submit-cancel": "Cancelar",
        "resetpass-wrong-oldpass": "La contraseña actual, o temporal, no es correcta.\nPuede que ya hayas cambiado tu contraseña o que hayas pedido una nueva contraseña temporal.",
-       "resetpass-recycled": "Restablece tu contraseña a algo distinto de tu contraseña actual.",
+       "resetpass-recycled": "Cambia tu contraseña a algo distinto de tu contraseña actual.",
        "resetpass-temp-emailed": "Has iniciado sesión con una contraseña temporal enviada por correo electrónico.\nPara continuar, debes establecer una nueva contraseña aquí:",
        "resetpass-temp-password": "Contraseña temporal:",
        "resetpass-abort-generic": "Una extensión ha cancelado el cambio de la contraseña.",
        "resetpass-expired": "Tu contraseña ha caducado. Por favor, establece una nueva contraseña para iniciar sesión.",
-       "resetpass-expired-soft": "Tu contraseña ha caducado, por lo que debes restablecerla. Cámbiala ahora por una nueva, o haz clic en \"{{int:authprovider-resetpass-skip-label}}\" para restablecerla más adelante.",
-       "resetpass-validity-soft": "Tu contraseña no es válida: $1\n\nCámbiala ahora por una nueva, o haz clic en \"{{int:authprovider-resetpass-skip-label}}\" para cambiarla más tarde.",
+       "resetpass-expired-soft": "Tu contraseña ha caducado, por lo que debes cambiarla. Elige ahora una contraseña nueva o pulsa en «{{int:authprovider-resetpass-skip-label}}» para cambiarla más tarde.",
+       "resetpass-validity-soft": "La contraseña no es válida: $1\n\nCámbiala ahora por una nueva, o bien, pulsa en «{{int:authprovider-resetpass-skip-label}}» para cambiarla más tarde.",
        "passwordreset": "Restablecer contraseña",
        "passwordreset-text-one": "Completa este formulario para recibir una contraseña temporal por correo electrónico.",
        "passwordreset-text-many": "{{PLURAL:$1|Rellena uno de los campos para recibir una contraseña temporal por correo electrónico.}}",
        "rcfilters-other-review-tools": "Otras herramientas de revisión",
        "rcfilters-group-results-by-page": "Agrupar resultados por página",
        "rcfilters-activefilters": "Filtros activos",
+       "rcfilters-activefilters-hide": "Ocultar",
+       "rcfilters-activefilters-show": "Mostrar",
        "rcfilters-advancedfilters": "Filtros avanzados",
        "rcfilters-limit-title": "Resultados que mostrar",
        "rcfilters-limit-and-date-label": "$1 {{PLURAL:$1|cambio|cambios}}, $2",
index 4816efa..3dd307e 100644 (file)
@@ -31,7 +31,8 @@
                        "Osoitz",
                        "Mikel Ibaiba",
                        "MarcoAurelio",
-                       "Iñaki LL"
+                       "Iñaki LL",
+                       "Amaia"
                ]
        },
        "tog-underline": "Azpimarratu loturak:",
        "cascadeprotected": "Orri hau aldaketen aurka babestua dago, barneratuta dagoelako «kaskadako babesa» aukerarekin babestu {{PLURAL:$1|duten orri honetan|dituzten orri hauetan}}:\n$2",
        "namespaceprotected": "Ez daukazu '''$1''' izen-tarteko orrialdeak aldatzeko baimenik.",
        "customcssprotected": "Ez duzu baimenik CSS orrialde hau aldatzeko beste erabiltzaile baten hobespen pertsonalak dituelako.",
+       "customjsonprotected": "Ez duzu baimenik JSON orrialde hau editatzeko beste erabiltzaile baten hobespen pertsonalak dituelako.",
        "customjsprotected": "Ez duzu baimenik JavaScript orrialde hau aldatzeko beste erabiltzaile baten hobespen pertsonalak dituelako.",
        "mycustomcssprotected": "Ez duzu baimenik CSS orrialde hau aldatzeko.",
        "mycustomjsonprotected": "Ez duzu baimenik JSON orrialde hau aldatzeko.",
        "wrongpasswordempty": "Pasahitza hutsik dago. Saiatu berriz.",
        "passwordtooshort": "Pasahitzek {{PLURAL:$1|karaktere 1|$1 karaktere}} gutxienez eduki behar dituzte.",
        "passwordtoolong": "Pasahitzak ezin dira {{PLURAL:$1|karaktere bat|$1 karaktere}} baino luzeagoak izan.",
-       "passwordtoopopular": "Ezin dira arrunki aukeratutako pasahitzak erabili. Aukera ezazu pasahitz originalago bat, mesedez.",
+       "passwordtoopopular": "Ezin dira pasahitz ohikoenak erabili. Aukera ezazu asmatzeko zailagoa den pasahitz bat.",
        "password-name-match": "Zure pasahitza ezin da zure erabiltzaile-izen bera izan.",
        "password-login-forbidden": "Erabiltzaile izen eta pasahitz hau erabiltzea debekaturik dago.",
        "mailmypassword": "Pasahitza berrezarri",
        "passwordremindertitle": "Pasahitzaren gogorarazpena {{SITENAME}}(e)tik",
-       "passwordremindertext": "Norbaitek (ziurrenik zuk, $1 IP helbidetik) pasahitz berri bat\neskatu du {{SITENAME}}(r)ako ($4). Momentu honetan erabiltzeko \"$2\" lankidearentzat\npasahitza sortu da eta \"$3\"(r)a aldatu da. Hau zuk eginiko saiakuntza bazen,\norain saioa hasi beharko duzu zure pasahitza berria aukeratzeko. Zure aldi baterako pasahitzak {{PLURAL:$5|egun baterako|$5 egunetarako}} baino ez du balio izango.\n\nBeste norbaitek eskari hau egin bazuen, edo zure pasahitza gogoratu baduzu,\neta ez baduzu aldatu nahi, mezu honetan irakurritakoari jaramonik ez egin\neta aurretik zenuen pasahitza erabiltzen jarrai ezazu.",
+       "passwordremindertext": "Norbaitek ($1 IP helbidetik) pasahitz berri bat\neskatu du {{SITENAME}}(r)ako ($4). Behin-behineko pasahitz berria sortu da \"$2\" erabiltzailearentzat eta \"$3\"(r)a aldatu da. Zuk eginiko saiakera balitz, jarraian\nsaioa hasi beharko duzu zure pasahitz berria aukeratzeko. Zure behin-behineko pasahitza {{PLURAL:$5|egun baterako|$5 egunetarako}} iraungiko da.\n\nEskari hau beste norbaitek egin izan balu, edo zure pasahitza gogoratu baduzu eta ez baduzu aldatu nahi, ez egin jaramonik mezu honetan irakurritakoari eta aurretik zenuen pasahitza erabiltzen jarrai ezazu.",
        "noemail": "Ez dago \"$1\" erabiltzailearen e-posta helbiderik gordeta.",
        "noemailcreate": "Balioduna den e-posta helbidea eman behar duzu",
        "passwordsent": "Pasahitz berria bidali da \"$1\" erabiltzailearen e-posta helbidera.\nMesedez, saioa hasi jasotakoan.",
        "botpasswords-existing": "Dauden bot-en pasahitzak",
        "botpasswords-createnew": "Sortu errobot pasaitza berri bat",
        "botpasswords-editexisting": "Aldatu lehendik dagoen errobot pasaitza",
+       "botpasswords-label-needsreset": "(pasahitza berrezarri behar da)",
        "botpasswords-label-appid": "Bot izena:",
        "botpasswords-label-create": "Sortu",
        "botpasswords-label-update": "Eguneratu",
        "botpasswords-restriction-failed": "Errobot pasahitza murrizketek logina saihesten dute.",
        "botpasswords-invalid-name": "Zehaztutako erabiltzaileak ez du bot pasahitzaren ($1) bereizlea.",
        "botpasswords-not-exist": "$1 erabiltzaileak ez du $2 izeneko pasahitza.",
+       "botpasswords-needs-reset": "\"$1\"{{GENDER:$1|erabiltzailearen}} \"$2\" robotaren pasahitza berrezarri behar da.",
        "resetpass_forbidden": "Ezin dira pasahitzak aldatu",
        "resetpass_forbidden-reason": "Ezin dira pasahitzak aldatu: $1",
        "resetpass-no-info": "Orrialde honetara zuzenean sartzeko izena eman behar duzu.",
        "resetpass-submit-loggedin": "Pasahitza aldatu",
        "resetpass-submit-cancel": "Utzi",
        "resetpass-wrong-oldpass": "Behin-behineko edo oraintxuko pasahitza ez da baliagarria.\nAgian dagoeneko aldatu duzu zure pasahitza edo behin-behineko pasahitza bat eskatu duzu.",
-       "resetpass-recycled": "Mesedez berritu zure pasahitza.",
+       "resetpass-recycled": "Mesedez aldatu zure pasahitza.",
        "resetpass-temp-emailed": "Aldi baterako posta elektronikoko kodea erabiliz saioa hasi duzu. \nSaio hastea amaitzeko, hemen pasahitz berria ezarri behar duzu:",
        "resetpass-temp-password": "Behin-behineko pasahitza:",
        "resetpass-abort-generic": "Estentsio batek pasahitza aldatzea ekidin du.",
        "resetpass-expired": "Zure pasahitza iraungitu da. Sartzeko, pasahitz berria ezarri, mesedez.",
-       "resetpass-expired-soft": "Zure pasahitza iraungi da eta berrezarri egin behar da. Aukeratu pasahitz berria orain edo egin klik \"{{int:authprovider-resetpass-skip-label}}\"-n geroago berrarazteko.",
-       "resetpass-validity-soft": "Zure pasahitzak ez du balio: $1\n\nAukeratu beste pasahitza berri bat orain mesedez, edo \"{{int:authprovider-resetpass-skip-label}}\" klikatu geroago berrezartzeko.",
+       "resetpass-expired-soft": "Zure pasahitza iraungi egin da eta aldatu beharra dago. Mesedez, aukeratu orain pasahitz berria edo egin klik \"{{int:authprovider-resetpass-skip-label}}\"-n geroago aldatzeko.",
+       "resetpass-validity-soft": "Zure pasahitzak ez du balio: $1\n\nMesedez, aukeratu orain pasahitz berri bat, edo \"{{int:authprovider-resetpass-skip-label}}\" klikatu geroago berrezartzeko.",
        "passwordreset": "Pasahitzaren berrezarpena",
        "passwordreset-text-one": "Bete formulario hau zure pasahitza berrezartzeko.",
        "passwordreset-text-many": "{{PLURAL:$1|Sartu datuetako bat zure pasahitza berrezartzeko.}}",
        "subject-preview": "Gaiaren aurrebista:",
        "previewerrortext": "Errore bat gertatu da aldaketak aurrezten saiatzean.",
        "blockedtitle": "Erabiltzailea blokeatuta dago",
-       "blockedtext": "'''Zure erabiltzaile izena edo IP helbidea blokeaturik dago.'''\n\n$1 administratzaileak ezarri du blokeoa.\nEmandako arrazoia hau da: ''$2''.\n\n* Blokeoaren hasiera: $8\n* Blokeoaren bukaera: $6\n* Blokeatua: $7\n\nBlokeoari buruz eztabaidatzeko, jo ezazu $1 administratzailearengana edo beste [[{{MediaWiki:Grouppage-sysop}}|administratzaile]] batengana.\n«Bidali mezu elektronikoa lankide honi» tresna erabili ahal izateko, ezinbestekoa da zure [[Special:Preferences|hobespenetan]] baliozkoa den helbide elektroniko bat emanda izatea, eta tresna hori erabiltzeko aukera zuri blokeatu ez izana.\nOrain duzun IP helbidea $3 da, eta blokeoaren zenbakia #$5 da.\nEman itzazu datu hauek guztiak, blokeoari buruzko edozein eskaera egitean.",
+       "blockedtext": "<strong> Zure erabiltzaile izena edo IP helbidea blokeatuta dago. </strong>\n\nBlokeoa $1-ek ezarri du.\nEmandako arrazoia hau da: ''$2''.\n\n* Blokeoaren hasiera: $8\n* Blokeoaren bukaera: $6\n* Blokeatua: $7\n\nBlokeoa eztabaidatzeko, $1 edo beste [[{{MediaWiki:Grouppage-sysop}}|administratzaile]] batekin jarri zaitezke kontaktuan.\nEzingo duzu \"{{int:emailuser}}\" funtzioa erabili[[Special:Preferences|hobespenetan]] baliozkoa den helbide elektroniko bat izan ezean, eta tresna hori erabiltzeko aukera blokeatuta ez baduzu.\nOrain duzun IP helbidea $3 da, eta blokeoaren zenbakia #$5.\nMesedez, eman aipatutako datu hauek guztiak, blokeoari buruzko edozein eskaera egitean.",
        "autoblockedtext": "Zure IP helbidea automatikoki blokeaturik dago, $1 administratzaileak blokeatutako beste wikilari batek erabili zuelako. Emandako arrazoia hau da:\n\n:''$2''\n\n* Blokeoaren hasiera: $8\n* Blokeoaren bukaera: $6\n* Blokeatua: $7\n\nBlokeoari buruz eztabaidatzeko, jo ezazu $1 administratzailearengana edo beste [[{{MediaWiki:Grouppage-sysop}}|administratzaile]] batengana.\n\n«Bidali mezu elektronikoa lankide honi» tresna erabili ahal izateko, ezinbestekoa da zure [[Special:Preferences|hobespenetan]] baliozkoa den helbide elektroniko bat emanda izatea, eta tresna hori erabiltzeko aukera zuri blokeatu ez izana.\n\nOrain duzun IP helbidea $3 da, eta blokeoaren zenbakia #$5 da.\n\nEman itzazu datu hauek guztiak, blokeoari buruzko edozein eskaera egitean.",
        "systemblockedtext": "Zure erabiltzaile izena edo IP helbidea MediaWiki-k automatikoki blokeatu du.\nHau da emandako arrazoia:\n\n:<em>$2</em>\n\n*Bloke sarrera: $8\n*Blokearen iraungintzea: $6\n*Blokeo helburua: $7\n\nZure uneko IP helbidea: $3.\nSartu goiko xehetasun guztiak egiten dituzun kontsulta guztietan mesedez.",
        "blockednoreason": "ez da arrazoirik zehaztu",
        "explainconflict": "Zu orrialdea aldatzen hasi ondoren beste norbaitek ere aldaketak egin ditu.\nGoiko testu koadroan ikus daiteke orrialdeak uneotan duen edukia.\nZure aldaketak beheko testu koadroan ikus daitezke.\nZure testua dagoenarekin elkartu beharko duzu.\nOrrialdea gordetzeko erabakitzen duzun unean goiko koadroko edukia '''bakarrik''' gordeko da.",
        "yourtext": "Zure testua",
        "storedversion": "Gordetako bertsioa",
-       "editingold": "'''KONTUZ: Artikulu honen bertsio zahar bat aldatzen ari zara. Gorde egiten baduzu, azkenengo aldaketa baino lehenagoko aldakuntzak, ezabatuak izango dira.'''",
+       "editingold": "<strong>KONTUZ: Artikulu honen bertsio zahar bat aldatzen ari zara.</strong> Gordetzen baduzu, bertsio hau baino geroago egindako aldaketa guztiak ezabatuko dira.",
        "unicode-support-fail": "Zure nabigatzaileak Unicode onartzen ez duela dirudi. Orrialdeak editatu behar dira, beraz, edizioa ez da gorde.",
        "yourdiff": "Ezberdintasunak",
        "copyrightwarning": "Kontuan izan ezazu {{SITENAME}} webgunean egindako ekarpen guztiak $2 lizentziaren pean argitaratzen direla (xehetasunetarako, ikus $1). Zuk idatzitakoa libreki aldatua eta banatua izatea nahi ez baduzu, ez ezazu hemen jarri.<br />\nEra berean, hitzematen ari zara hau zuk zeuk idatzia dela, edo jabari publikotik nahiz askea den beste ituri batetik kopiatu duzula.\n'''Ez erabili copyright eskubideek babestutako lanik, baimenik gabe!'''",
index 4614b74..a81a622 100644 (file)
                        "Vexthedorito",
                        "Djiboun",
                        "Pols12",
-                       "KATRINE1992"
+                       "KATRINE1992",
+                       "Friday83260"
                ]
        },
        "tog-underline": "Soulignement des liens :",
        "resetpass-submit-loggedin": "Changer de mot de passe",
        "resetpass-submit-cancel": "Annuler",
        "resetpass-wrong-oldpass": "Mot de passe actuel ou temporaire invalide.\nVous avez peut-être déjà changé votre mot de passe ou demandé un nouveau mot de passe temporaire.",
-       "resetpass-recycled": "Veuillez réinitialiser votre mot de passe à quelque chose d’autre que l’actuel.",
+       "resetpass-recycled": "Veuillez modifier votre mot de passe à quelque chose d’autre que l’actuel.",
        "resetpass-temp-emailed": "Vous êtes connecté{{GENDER:||e}} avec un code temporaire fourni par courriel.\nPour terminer la connexion, vous devez fournir un nouveau mot de passe ici :",
        "resetpass-temp-password": "Mot de passe temporaire :",
        "resetpass-abort-generic": "La modification du mot de passe a été annulée par une extension.",
        "resetpass-expired": "Votre mot de passe a expiré. Veuillez en fournir un nouveau pour vous connecter.",
-       "resetpass-expired-soft": "Votre mot de passe a expiré, et doit être réinitialisé. Veuillez en choisir un nouveau maintenant ou cliquer sur « {{int:authprovider-resetpass-skip-label}} » pour le faire plus tard.",
-       "resetpass-validity-soft": "Votre mot de passe n’est pas valide : $1\n\nVeuillez choisir un nouveau mot de passe maintenant, ou cliquez sur « {{int:authprovider-resetpass-skip-label}} » pour le réinitialiser plus tard.",
+       "resetpass-expired-soft": "Votre mot de passe a expiré, et doit être modifié. Veuillez en choisir un nouveau maintenant ou cliquer sur « {{int:authprovider-resetpass-skip-label}} » pour le faire plus tard.",
+       "resetpass-validity-soft": "Votre mot de passe n’est pas valide : $1\n\nVeuillez choisir un nouveau mot de passe maintenant, ou cliquez sur « {{int:authprovider-resetpass-skip-label}} » pour le modifier plus tard.",
        "passwordreset": "Réinitialisation du mot de passe",
        "passwordreset-text-one": "Remplissez ce formulaire pour réinitialiser votre mot de passe.",
        "passwordreset-text-many": "{{PLURAL:$1|Remplissez un des champs pour recevoir un mot de passe temporaire par courriel.}}",
        "rcfilters-other-review-tools": "Autres outils de relecture",
        "rcfilters-group-results-by-page": "Grouper les résultats par page",
        "rcfilters-activefilters": "Filtres actifs",
+       "rcfilters-activefilters-hide": "Masquer",
+       "rcfilters-activefilters-show": "Afficher",
        "rcfilters-advancedfilters": "Filtres avancés",
        "rcfilters-limit-title": "Résultats à afficher",
        "rcfilters-limit-and-date-label": "$1 modification{{PLURAL:$1||s}}, $2",
        "rcfilters-empty-filter": "Aucun filtre actif. Toutes les contributions sont affichées.",
        "rcfilters-filterlist-title": "Filtres",
        "rcfilters-filterlist-whatsthis": "Comment ça marche ?",
-       "rcfilters-filterlist-feedbacklink": "Nous dire ce que vous pensez de ces (nouveaux) outils de filtrage",
+       "rcfilters-filterlist-feedbacklink": "Dites nous ce que vous pensez de ces outils de filtrage",
        "rcfilters-highlightbutton-title": "Mettre en valeur les résultats",
        "rcfilters-highlightmenu-title": "Choisir une couleur",
        "rcfilters-highlightmenu-help": "Sélectionner une couleur pour mettre en évidence cette propriété",
        "rcfilters-preference-label": "Masquer la version améliorée des modifications récentes",
        "rcfilters-preference-help": "Désactive la version 2017 de l'interface ainsi que de tous les outils ajoutés alors et depuis.",
        "rcfilters-watchlist-preference-label": "Masquer la version améliorée de la liste de suivi",
+       "rcfilters-watchlist-preference-help": "Annuler la nouvelle conception de l’interface 2017 et tous les outils ajoutés alors et depuis.",
        "rcfilters-filter-showlinkedfrom-label": "Montrer les modifications des pages liées depuis",
        "rcfilters-filter-showlinkedfrom-option-label": "<strong>Pages liées depuis</strong> la page sélectionnée",
        "rcfilters-filter-showlinkedto-label": "Montrer les modifications des pages pointant vers",
index 2660604..d1e6885 100644 (file)
        "newwindow": "(Ka ouvri so kò andan roun nouvèl finèt)",
        "cancel": "Anilé",
        "moredotdotdot": "Plis...",
-       "morenotlisted": "Sa lis pé sa enkonplèt",
+       "morenotlisted": "Sa lis pouvé fika enkonplèt",
        "mypage": "Paj",
        "mytalk": "Diskisyon",
        "anontalk": "Diskisyon",
        "tagline": "Di {{SITENAME}}",
        "help": "Èd",
        "search": "Sasé",
-       "search-ignored-headings": " #<!-- pa modifyé sa lign --><pre>\n# Tit dé sèksyon ki sa ignoré pa sasé-a.\n# Chanjman éfèktchwé isi ka pran éfè lò ki paj-a ké tit-a sa endèksé.\n# Zòt pé fòrsé réyendèksasyon di paj-a an éfèktchwan oun modifikasyon vid.\n# Sentaks-a sa swivant :\n#   * Tousa ki ka swiv roun « # » jouk finisman di lign-a sa roun koumantèr.\n#   * Tout lign ki pa-vid sa tit ègzak à ignoré, kas konprann osi.\nRéférans\nLyen èkstèrn\nWè osi\n #</pre><!-- pa modifyé sa lign -->",
+       "search-ignored-headings": " #<!-- pa modifyé sa lign --><pre>\n# Tit dé sèksyon ki ké fika ignoré pa sasé-a.\n# Chanjman-yan ki éfèktchwé isi ka pran léfè lò ki paj-a ké tit-a sa endèksé.\n# Zòt pouvé fòrsé réyendèksasyon di paj-a an éfèktchwan roun modifikasyon vid.\n# Sentaks-a sa swivant-a :\n#   * Tousa ki ka swiv roun « # » jouk finisman-an di lign-an sa roun koumantèr.\n#   * Tout lign ki pa-vid sa tit ègzak-a pou ignoré, kas konprann osi.\nRéférans\nLyen ègstèrn\nWè osi\n #</pre><!-- pa modifyé sa lign -->",
        "searchbutton": "Sasé",
        "go": "Konsilté",
        "searcharticle": "Kontinwé",
        "nstab-category": "Katégori",
        "mainpage-nstab": "Paj prensipal",
        "nosuchaction": "Aksyon enkonèt",
-       "nosuchactiontext": "Aksyon-an spésifyé andan URL-a sa envalid.\nZòt pitèt mal antré URL-a ou swivi roun lyen éroné.\nLi pé égalman endiké oun anomali andan logisyèl itilizé pa {{SITENAME}}.",
+       "nosuchactiontext": "Aksyon-an èspésifyé annan URL-a sa envalid.\nZòt pitèt mal antré URL-a oben swivi roun lyen éroné.\nLi pouvé égalman endiké oun anomali andan logisyèl itilizé pa {{SITENAME}}.",
        "nosuchspecialpage": "Paj èspésyal inègzistant",
        "nospecialpagetext": "<strong>Zòt doumandé oun paj èspésyal ki pa ka ègzisté.</strong>\n\nOun lis dé paj èspésyal valid ka trouvé so kò asou [[Special:SpecialPages|{{int:specialpages}}]].",
        "error": "Érò",
        "databaseerror": "Érò di baz di doné",
-       "databaseerror-text": "Oun érò di rékèt di baz di doné aparèt.\nSala pé provini di roun anomali annan lojisyèl-a.",
+       "databaseerror-text": "Oun érò di rékèt di baz di doné aparèt.\nSala pouvé provini di roun anomali annan lojisyèl-a.",
        "databaseerror-textcl": "Oun érò di rékèt di baz di doné aparèt.",
        "databaseerror-query": "Rékèt : $1",
        "databaseerror-function": "Fonksyon : $1",
        "databaseerror-error": "Érò : $1",
        "transaction-duration-limit-exceeded": "Pou évité roun tròp fò ogmantasyon di délè di réplikasyon, sa tranzaksyon té anilé piskétan douré di ékritir ($1) dépasé limit-a di $2 ségonn. Si zòt ka sasé modifyé oun gran nonm di éléman similtanéman, éséyé plito di éfèktchwé opérasyon-an an plizyò étap pli piti.",
-       "laggedslavemode": "Panga, sa paj pa pé kontni tout dannyé modifikasyon éfèktchwé",
+       "laggedslavemode": "Panga, sa paj pa pouvé kontni tout dannyé modifikasyon éfèktchwé",
        "readonly": "Baz di doné vérouyé",
        "enterlockreason": "Endiké rézon-an di vérouyaj ensi ki roun èstimasyon di so douré",
        "readonlytext": "Ajou ké mizajou di baz di doné sa atchwèlman bloké, probabman pou pèrmèt mentnans di baz-a, aprè sa, tout bagaj ké rantré andan lòrd.\n\nAdministratò sistèm ki vérouyé baz di doné té fourni èksplikasyon swivant :<br /> $1",
        "filenotfound": "Enposib di trouvé fiché-a « $1 ».",
        "unexpected": "Valò ki pa nòrmal : « $1 » = « $2 ».",
        "formerror": "Érò : enposib di soumèt fòrmilèr-a.",
-       "badarticleerror": "Sa aksyon pa pé sa éfèktchwé asou sa paj.",
+       "badarticleerror": "Sa aksyon pa pouvé fika éfèktchwé asou sa paj.",
        "cannotdelete": "Enposib di souprimé paj-a oben fiché-a « $1 ».\nSouprésyon-an pitèt ja té éfèktchwé pa rounòt moun.",
        "cannotdelete-title": "Enposib di souprimé paj-a « $1 »",
        "delete-hook-aborted": "Souprésyon anilé pa roun ègstansyon.\nPyès èksplikasyon té bay.",
        "no-null-revision": "Enposib di kréyé roun nouvèl révizyon vid pou paj-a « $1 »",
        "badtitle": "Movè tit",
-       "badtitletext": "Tit di paj doumandé pa valid, vid, ou mal formé si a roun tit entèr-lanng ou entèr-projè.\nI ka kontni pitèt oun ou plizyò karaktèr ki pa pé sa itilizé andan tit-ya.",
+       "badtitletext": "Tit di paj doumandé pa valid, vid, oben mal fòrmé si a roun tit entèr-lanng oben entèr-projè.\nI ka kontni pitèt oun oben plizyò karaktèr ki pa pouvé fika itilizé annan tit-ya.",
        "title-invalid-empty": "Tit di paj doumandé sa vid oben ka kontni sèlman non-an di roun lèspas di non.",
        "title-invalid-utf8": "Tit di paj doumandé ka kontni roun sékans UTF-8 envalid.",
        "title-invalid-interwiki": "Paj sib ka kontni roun lyen interwiki ki nou pa pouvé itilizé annan tit-ya.",
-       "title-invalid-talk-namespace": "Tit di paj doumandé ka fè référans à roun paj di diskisyon ki pa pé ègzisté.",
+       "title-invalid-talk-namespace": "Tit di paj doumandé ka fè référans à roun paj di diskisyon ki pa pouvé ègzisté.",
        "title-invalid-characters": "Tit di paj doumandé ka kontni dé karaktèr ki pa valid : « $1 ».",
        "title-invalid-relative": "Tit ka kontni oun chimen roulatif. Tit-ya ki ka référansé dé paj roulativ (./, ../) pa valid pas li sa souvan itilizé pa navigatò di itilizatò-a.",
        "title-invalid-magic-tilde": "Tit di paj doumandé ka kontni roun sékans di tilde majik ki pa valid (<nowiki>~~~</nowiki>).",
        "actionthrottled": "Aksyon limité",
        "actionthrottledtext": "Pou briga kont abi-ya, itilizasyon-an di sa aksyon sa limité à roun sèrten nonm di fwè annan roun laps di tan asé kourt é zòt dépasé sa limit.\nSouplé, éséyé òkò annan tchèk minout.",
        "protectedpagetext": "Sa paj té protéjé pou anpéché so modifikasyon oben dé ròt aksyon.",
-       "viewsourcetext": "Zòt pé wè é kopyé kontni di sa paj.",
+       "viewsourcetext": "Zòt pouvé wè é kopyé kontni-a di sa paj.",
        "viewyourtext": "Zòt pouvé wè ké kopyé kontni-a di <strong>zòt modifikasyon</strong> à sa paj.",
        "protectedinterface": "Sa paj ka fourni tèks d'entèrfas pou lojisyèl-a asou sa wiki é sa protéjé pou évité abi-ya.\nPou ajouté oben modifyé dé anmòrfwézaj asou tout wiki, souplé, itilizé [https://translatewiki.net/ translatewiki.net], projè-a di réjyonalizasyon di MediaWiki.",
        "editinginterface": "<strong>Panga :</strong> zòt ka modifiyé oun paj itilizé pou kréyé tèks-a di lojisyèl.\nChanjman-yan asou sa paj ké répèrkité asou aparans di entèrfas itilizatò pou ròt itilizatò-ya di sa wiki.",
        "createacct-email-ph": "Zòt adrès di kouryé",
        "createacct-another-email-ph": "Antré adrès-a di kouryé",
        "createaccountmail": "Itilizé roun mo di pas aléyatwar tanporèr é voyé li pou adrès-a di kouryé spésifyé",
-       "createaccountmail-help": "Pé sa itilizé pou kréyé roun kont pou rounòt moun san konèt mo di pas.",
+       "createaccountmail-help": "Pouvé fika itilizé pou kréyé roun kont pou rounòt moun san konèt mo di pas-a.",
        "createacct-realname": "Non réyèl (fakiltatif)",
        "createacct-reason": "Motif",
        "createacct-reason-ph": "Poukisa zòt kréyé rounòt kont",
        "botpasswords-label-delete": "Souprimé",
        "botpasswords-label-resetpassword": "Réyinisyalizé mo di pas",
        "botpasswords-label-grants": "Drwè aplikab :",
-       "botpasswords-help-grants": "Otorizasyon-yan ka pèrmèt di aksédé o drwè ki ja akòrdé à zòt kont itilizatò. Aktivé roun otorizasyon isi pa ka fourni laksè à pyès drwè ki zòt kont itilizatò pé ké gen dayò. Wè [[Special:ListGrants|tablo dé otorizasyon]] pou plis d’enfòrmasyon.",
+       "botpasswords-help-grants": "Otorizasyon-yan ka pèrmèt di aksédé o drwè ki déja akòrdé à zòt kont itilizatò. Aktivé roun otorizasyon isi ka fourni laksè à pyès drwè ki zòt kont itilizatò pa gen dayò. Wè [[Special:ListGrants|tablo dé otorizasyon]] pou plis d’enfòrmasyon.",
        "botpasswords-label-grants-column": "Akòrdé",
        "botpasswords-bad-appid": "Non-an di robo « $1 » pa valid.",
        "botpasswords-insert-failed": "Échèk di ajou-a di non di robo « $1 ». Ès i té ja ajouté ?",
        "showpreview": "Prévizwalizé",
        "showdiff": "Wè modifikasyon-yan",
        "anoneditwarning": "<strong>Panga :</strong> zòt pa konèkté. Zòt adrès IP ké sa vizib di tout moun si zòt ka fè dé modifikasyon. Si zòt <strong>[$1 ka konèkté zòt kò]</strong> ou <strong>[$2 kréyé roun kont]</strong>, zòt modifikasyon ké sq atribwé à zòt pròp non di itilizatò(ris) é zòt ké gen dé ròt avantaj.",
-       "blockedtext": "<strong>Zòt kont itilizatò oben zòt adrès IP bloké.</strong>\n\nBlokaj té éfèktchwé pa $1.\nRézon-an évoké sa swivant : <em>$2</em>.\n\n* Koumansman di blokaj : $8\n* Èspirasyon di blokaj : $6\n* Kont bloké : $7.\n\nZòt pé kontakté $1 oben rounòt [[{{MediaWiki:Grouppage-sysop}}|administratò]] pou an diskité.\nZòt pa pouvé itilizé fonksyon-an « {{int:emailuser}} » rounso si oun adrès di kouryé valid sa èspésifyé andan zòt [[Special:Preferences|préférans]] é rounso si sa fonksyonalité pa bloké.\nZòt adrès IP atchwèl sa $3 é zòt idantifyan di blokaj sa $5.\nSouplé, enkli tout détay-ya lasou'l annan chakin dé rékèt ki zòt ké fè.",
+       "blockedtext": "<strong>Zòt kont itilizatò oben zòt adrès IP bloké.</strong>\n\nBlokaj té éfèktchwé pa $1.\nRézon-an ki évoké ka swiv : <em>$2</em>.\n\n* Koumansman di blokaj : $8\n* Èspirasyon di blokaj : $6\n* Kont bloké : $7.\n\nZòt pouvé kontakté $1 oben rounòt [[{{MediaWiki:Grouppage-sysop}}|administratò]] pou an diskité.\nZòt pa pouvé itilizé fonksyon-an « {{int:emailuser}} » rounso si oun adrès di kouryé valid sa èspésifyé andan zòt [[Special:Preferences|préférans]] é rounso si sa fonksyonalité pa bloké.\nZòt adrès IP atchwèl sa $3 é zòt idantifyan di blokaj sa $5.\nSouplé, enkli tout détay-ya lasou'l annan chakin dé rékèt ki zòt ké fè.",
        "loginreqlink": "konèkté so kò",
-       "newarticletext": "Zòt té ka swiv roun lyen vèr roun paj ki pa ka ègzisté òkò. \nAtò di kréyé sa paj, antré zòt tèks annan bwat ki aprè (zòt pé konsilté [$1 paj d'èd-a] pou plis enfòrmasyon).\nSi zòt pa rivé{{GENDER:|}} isi pa éròr, kliké asou bouton <strong>Routour</strong> di zòt navigatò.",
-       "anontalkpagetext": "----\n<em>Zòt asou paj di diskisyon di oun itilizatò anonim ki pa òkò kréyé di kont ou ki pa ka an itilizé</em>.\nPou sa rézon, nou divèt itilizé so adrès IP pou idantifyé li.\nOun adrès IP pé sa partajé pa plizyò itilizatò.\nSi zòt roun itiliza{{GENDER:|ò|ris}} anonim é si zòt ka kontasté ki dé koumantèr ki pa ka konsèrné zòt sa adrèsé à zòt, zòt pé [[Special:CreateAccount|kréyé roun kont]] ou [[Special:UserLogin|konèkté zòt kò]] atò di évité tout konfizyon fitir ké ròt kontribitò anonim.",
-       "noarticletext": "I pa gen atchwèlman pyès tèks asou sa paj.\nZòt pouvé [[Special:Search/{{PAGENAME}}|lansé oun sasé asou sa tit]] annan ròt paj-ya,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} sasé annan opérasyon lyé]\noben [{{fullurl:{{FULLPAGENAME}}|action=edit}} kréyé sa paj]</span>.",
+       "newarticletext": "Zòt té ka swiv roun lyen bò'd roun paj ki pa ka ègzisté òkò. \nAfen di kréyé sa paj, antré zòt tèks annan bwèt ki aprè (zòt pouvé konsilté [$1 paj di lèd-a] pou plis d'enfòrmasyon).\nSi zòt vini{{GENDER:|}} isi pa éròr, kliké asou bouton <strong>Routour</strong> di zòt navigatò.",
+       "anontalkpagetext": "----\n<em>Zòt asou paj di diskisyon di roun itilizatò anonim ki pa òkò kréyé di kont oben ki pa ka an itilizé</em>.\nPou sa rézon, nou divèt itilizé so adrès IP pou idantifyé li.\nOun adrès IP pouvé fika partajé pa plizyò itilizatò.\nSi zòt roun itiliza{{GENDER:|ò|ris}} anonim é si zòt ka kontasté ki dé koumantèr ki pa ka konsèrné zòt sa adrèsé pou zòt, zòt pouvé [[Special:CreateAccount|kréyé roun kont]] oben [[Special:UserLogin|konèkté zòt kò]] atò di évité tout konfizyon fitir ké ròt kontribitò anonim.",
+       "noarticletext": "I pa gen atchwèlman pyès tèks asou sa paj.\nZòt pouvé [[Special:Search/{{PAGENAME}}|lansé oun sasé asou sa tit]] annan ròt paj-ya,\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} sasé annan opérasyon ki lyannen]\noben [{{fullurl:{{FULLPAGENAME}}|action=edit}} kréyé sa paj]</span>.",
        "noarticletext-nopermission": "I pa gen atchwèlman pyès tèks asou sa paj.\nZòt pouvé [[Special:Search/{{PAGENAME}}|fè roun sasé asou sa tit]] andan ròt paj-ya,\noben <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|paj={{FULLPAGENAMEE}}}} sasé annan journal asosyé]</span>, mè zòt pa gen pèrmisyon di kréyé sa paj.",
        "userpage-userdoesnotexist-view": "Kont itilizatò-a « $1 » pa anréjistré.",
        "clearyourcache": "<strong>Nòt :</strong> aprè zòt anréjistré zòt modifikasyon, zòt divèt forsé roucharjman konplè di kach di zòt navigatò pou wè chanjman-yan.\n* <strong>Firefox / Safari :</strong> mentné touch-a <em>Maj</em> (<em>Shift</em>) an klikan asou bouton-an <em>Atchwalizé</em> ou présé <em>Ctrl-F5</em> ou <em>Ctrl-R</em> (<em>⌘-R</em> asou roun Mac) \n* <strong>Google Chrome :</strong> apwiyé asou <em>Ctrl-Maj-R</em> (<em>⌘-Shift-R</em> asou roun Mac) \n* <strong>Internet Explorer :</strong> mentné touch-a <em>Ctrl</em> an klikan asou bouton-an <em>Atchwalizé</em> ou présé <em>Ctrl-F5</em> \n* <strong>Opera :</strong> alé annan <em>Menu → Settings</em> (<em>Opera → Préférences</em> asou roun Mac) é answit à <em>Konfidansyalité & sékrité → Éfasé doné d'èksplorasyon-yan → Imaj ké fiché an kach</em>.",
        "template-semiprotected": "(sémi-protéjé)",
        "hiddencategories": "{{PLURAL:$1|Katégori kaché}} don sa paj ka fè parti :",
        "permissionserrors": "Éròr di pèrmisyon",
-       "permissionserrorstext-withaction": "Zòt pa pé $2, pou {{PLURAL:$1|rézon swivant}} :",
+       "permissionserrorstext-withaction": "Zòt pa pouvé $2, pou {{PLURAL:$1|rézon swivant}} :",
        "recreate-moveddeleted-warn": "<strong>Panga : zòt ka roukréyé oun paj ki té présédaman souprimé.</strong>\n\nAsouré-zòt ki li sa pertinan di pourswiv modifikasyon-yan asou sa paj.\nJournal dé souprésyon é dé déplasman pou sa paj sa afiché anba à tit di enfòrmasyon :",
        "moveddeleted-notice": "Sa paj té souprimé. \nJournal dé souprésyon, dé protèksyon é dé déplasman di paj-a sa afiché anba pou référans.",
        "content-model-wikitext": "wikitèks",
-       "undo-failure": "Sa modifikasyon pa pé défèt : sala ké antré an konfli ké modifikasyon-yan entèrmédyèr.",
+       "undo-failure": "Sa modifikasyon pa pouvé défèt : sala ké antré an konfli ké modifikasyon entèrmédjèr-ya.",
        "viewpagelogs": "Wè opérasyon-yan asou sa paj",
        "currentrev-asof": "Vèrsyon atchwèl daté di $1",
        "revisionasof": "Vèrsyon di $1",
        "boteditletter": "b",
        "rc-change-size-new": "$1 {{PLURAL:$1|oktè}} aprè chanjman",
        "rc-old-title": "kréyé inisyalman ké tit « $1 »",
-       "recentchangeslinked": "Swivi dé paj lyé",
-       "recentchangeslinked-feed": "Swivi dé paj lyé",
-       "recentchangeslinked-toolbox": "Swivi dé paj lyé",
+       "recentchangeslinked": "Swivi dé paj ki lyannen",
+       "recentchangeslinked-feed": "Swivi dé paj ki lyannen",
+       "recentchangeslinked-toolbox": "Swivi dé paj ki lyannen",
        "recentchangeslinked-title": "Swivi dé paj asosyé à « $1 »",
-       "recentchangeslinked-summary": "Antré roun non di paj pou wè modifikasyon-yan ki fè résaman asou dé paj lyé dipi oben bò'd sa paj (pou wè manm-yan di oun katégori, antré {{ns:category}}:Non di katégori). Modifikasyon-yan dé paj di [[Special:Watchlist|zòt lis di swivi]] sa <strong>an gra</strong>.",
+       "recentchangeslinked-summary": "Antré roun non di paj pou wè modifikasyon-yan ki fè résaman asou dé paj ki lyannen dipi oben bò'd sa paj (pou wè manm-yan di oun katégori, antré {{ns:category}}:Non di katégori). Modifikasyon-yan dé paj di [[Special:Watchlist|zòt lis di swivi]] sa <strong>an gra</strong>.",
        "recentchangeslinked-page": "Non di paj :",
        "recentchangeslinked-to": "Afiché modifikasyon-yan dé paj ki ka konpòrté roun lyen vèr paj ki bay plito ki envèrs",
        "upload": "Enpòrté roun fiché",
        "linkstoimage-more": "Plis {{PLURAL:$1|di roun paj ka itilizé|di $1 paj ka itilizé}} sa fiché.\nLis swivant ka afiché sèlman {{PLURAL:$1|pronmyé paj-a ki ka itilizé|$1 pronmyé paj-ya ki ka itilizé}} sa fiché.\nOun [[Special:WhatLinksHere/$2|lis konplèt]] sa disponib.",
        "nolinkstoimage": "Pyès paj pa ka itilizé sa fiché.",
        "linkstoimage-redirect": "$1 (roudirèksyon di fiché) $2",
-       "sharedupload-desc-here": "Sa fiché ka provini di $1. Li pé sa itilizé pa dé ròt projè.\nSo dèskripsyon asou so [$2 paj di dèskripsyon] sa afiché anba.",
+       "sharedupload-desc-here": "Sa fiché ka provini di $1. Li pouvé fika itilizé pa ròt projè.\nSo dèskripsyon asou so [$2 paj di dèskripsyon] sa afiché anba.",
        "filepage-nofile": "Pyès fiché di sa non ka ègzisté.",
-       "upload-disallowed-here": "Zòt pa pé ranplasé sa fiché.",
+       "upload-disallowed-here": "Zòt pa pouvé ranplasé sa fiché.",
        "randompage": "Paj o azò",
        "statistics": "Èstatistik",
        "double-redirect-fixer": "Korèktò di roudirèksyon",
        "speciallogtitlelabel": "Sib (tit ou {{ns:user}}:non di itilizatò) :",
        "log": "Journal d’opérasyon",
        "all-logs-page": "Tout journal piblik",
-       "alllogstext": "Afichaj konbiné di tout journal disponib asou {{SITENAME}}.\nZòt pé pèrsobalizé afichaj an sélèksyonan tip di journal, non di itilizatò ou paj-a konserné (sa dé dannyé sa sansib à lakas).",
+       "alllogstext": "Afichaj konbiné di tout journal disponib asou {{SITENAME}}.\nZòt pouvé pèrsonalizé afichaj an sélèksyonnan tip di journal, non di itilizatò oben paj-a ki konsèrné (sa Dé dannyé sa sansib à lakas).",
        "logempty": "Pyès opérasyon korèspondant andan journal-ya.",
        "allpages": "Tout paj-ya",
        "allarticles": "Tout paj-ya",
        "sp-contributions-toponly": "Montré ki kontribisyon-yan ki sa dannyé-ya dé artik",
        "sp-contributions-newonly": "Afiché inikman modifikasyon-yan ki sa dé kréyasyon di paj",
        "sp-contributions-submit": "Sasé",
-       "whatlinkshere": "Paj lyé",
+       "whatlinkshere": "Paj ki lyannen",
        "whatlinkshere-title": "Paj ki ka pwenté bò'd « $1 »",
        "whatlinkshere-page": "Paj :",
        "linkshere": "Paj-ya ki anba ka kontni roun lyen vèr <strong>$2</strong> :",
        "tooltip-ca-talk": "Diskisyon o sijè di sa paj di kontni",
        "tooltip-ca-edit": "Modifyé wikikod-a",
        "tooltip-ca-addsection": "Koumansé roun nouvèl sèksyon",
-       "tooltip-ca-viewsource": "Sa paj sa protéjé.\nZòt pé toutfwè vizwalizé sours-a.",
+       "tooltip-ca-viewsource": "Sa paj sa protéjé.\nZòt pouvé toutfwè vizwalizé sours-a.",
        "tooltip-ca-history": "Istorik vèrsyon-yan di sa paj",
        "tooltip-ca-protect": "Protéjé sa paj",
        "tooltip-ca-delete": "Souprimé sa paj",
        "tooltip-p-logo": "Vizité paj prensipal-a",
        "tooltip-n-mainpage": "Vizité paj prensipal-a di sit",
        "tooltip-n-mainpage-description": "Paj prensipal jénéral",
-       "tooltip-n-portal": "À propo di projè, sa ki zòt pé fè, koté trouvé enfòrmasyon-yan",
+       "tooltip-n-portal": "À propo di projè, sa ki zòt pouvé fè, koté trouvé enfòrmasyon-yan",
        "tooltip-n-currentevents": "Trouvé plis d'enfòrmasyon asou atchwalité an kour",
        "tooltip-n-recentchanges": "Lis di modifikasyon résant asou wiki-a",
        "tooltip-n-randompage": "Afiché roun paj o azò",
        "tooltip-n-help": "Aksè à lèd",
-       "tooltip-t-whatlinkshere": "Lis di paj lyé ki ka pwenté asou sala",
+       "tooltip-t-whatlinkshere": "Lis dé paj ki lyannen ki ka pwenté asou sala",
        "tooltip-t-recentchangeslinked": "Lis di modifikasyon résant liyé à sa paj",
        "tooltip-feed-atom": "Flux Atom pou sa paj",
        "tooltip-t-contributions": "Wè lis dé kontribisyon di {{GENDER:$1|sa itilizatò|sa itilizatris}}",
        "show-big-image-other": "{{PLURAL:$2|Ròt rézolisyon}} : $1.",
        "show-big-image-size": "$1 × $2 piksèl",
        "metadata": "Métadoné",
-       "metadata-help": "Sa fiché ka kontni dé enfòrmasyon siplémantèr, probabman ajouté pa aparèy foto nimèrik ou nimérizò itilizé pou kréyé. \nSi fiché té modifyé dipi so léta orijinal, serten détay pa pé rouflété antyèrman imaj modifyé-a.",
+       "metadata-help": "Sa fiché ka kontni dé enfòrmasyon siplémantèr, probabman ajouté pa aparèy foto nimèrik-a oben nimérizò-a ki itilizé pou kréyé. \nSi fiché-a té modifyé dipi so léta orijinal, sèrten détay pa pouvé rouflété antchèrman imaj-a ki modifyé.",
        "metadata-fields": "Chan di métadoné d'imaj listé andan sa mésaj ké sa enkli andan paj di dèskripsyon di imaj-a lò tab-a di métadoné ké sa rédjwit. Ròt chan ké sa kaché pa défo.\n* make\n* model\n* datetimeoriginal\n* exposuretime\n* fnumber\n* isospeedratings\n* focallength\n* artist\n* copyright\n* imagedescription\n* gpslatitude\n* gpslongitude\n* gpsaltitude",
        "exif-orientation": "Oryantasyon",
        "exif-xresolution": "Rézolisyon orizontal",
index dc96b30..cf992cc 100644 (file)
        "resetpass-temp-password": "Contrasinal temporal:",
        "resetpass-abort-generic": "Unha extensión cancelou a modificación do contrasinal.",
        "resetpass-expired": "O seu contrasinal caducou. Defina un novo contrasinal para acceder.",
-       "resetpass-expired-soft": "O seu contrasinal caducou e debe restablecelo. Escolla un novo contrasinal ou prema en \"{{int:authprovider-resetpass-skip-label}}\" para restablecelo máis tarde.",
+       "resetpass-expired-soft": "O seu contrasinal caducou e debe mudalo. Escolla un novo contrasinal ou prema en \"{{int:authprovider-resetpass-skip-label}}\" para mudalo máis tarde.",
        "resetpass-validity-soft": "O seu contrasinal non é válido: $1\n\nEscolla un novo contrasinal agora ou prema en \"{{int:authprovider-resetpass-skip-label}}\" para restablecelo máis tarde.",
        "passwordreset": "Restablecer o contrasinal",
        "passwordreset-text-one": "Encha este formulario para restablecer o seu contrasinal.",
index 8e5df68..24106ce 100644 (file)
@@ -12,6 +12,7 @@
                        "Amire80"
                ]
        },
+       "tog-underline": "𐌲𐌰𐍅𐌹𐍃𐌰𐌹𐍃 𐌿𐍆𐍃𐍄𐍂𐌹𐌺𐍃:",
        "tog-hideminor": "𐌰𐍆𐍆𐌹𐌻𐌷 𐌻𐌴𐌹𐍄𐌹𐌻𐍉𐍃 𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹𐌽𐍉𐍃 𐌰𐌽𐌳𐍅𐌰𐌹𐍂𐌸𐌰𐌹𐌶𐍉 𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹𐌽𐍉",
        "tog-extendwatchlist": "𐌿𐍆𐍂𐌰𐌺𐌴𐌹 𐍅𐌹𐍄𐌰𐍅𐌹𐌺𐍉𐌽 𐌳𐌿 𐌱𐌰𐌽𐌳𐍅𐌾𐌰𐌽 𐌰𐌻𐌻𐍉𐍃 𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹𐌽𐍉𐍃, 𐌽𐌹 𐌸𐌰𐍄𐌰𐌹𐌽𐌴𐌹 𐌸𐍉𐍃 𐌰𐍆𐍄𐌿𐌼𐌹𐍃𐍄𐍉𐌽𐍃",
        "tog-usenewrc": "𐌺𐌿𐌽𐌾𐌴 𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹𐌽𐍉𐍃 𐌰𐍆𐌰𐍂 𐌻𐌰𐌿𐌱𐌰 𐌹𐌽 𐌰𐍆𐍄𐌿𐌼𐌹𐍃𐍄𐌰𐌹𐌼 𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹𐌽𐌹𐌼 𐌰𐌽𐌰 𐍅𐌹𐍄𐌰𐍅𐌹𐌺𐍉𐌽",
        "category-subcat-count": "{{PLURAL:$2|𐌸𐌰𐍄𐌰 𐌺𐌿𐌽𐌹 𐌷𐌰𐌱𐌰𐌹𐌸 𐌸𐌰𐍄𐌴𐌹𐌽𐌴𐌹 𐌹𐍆𐍄𐌿𐌼 𐌼𐌹𐌽𐌽𐌹𐌶𐍉𐌽 𐌺𐌿𐌽𐌹|𐌸𐌰𐍄𐌰 𐌺𐌿𐌽𐌹 𐌷𐌰𐌱𐌰𐌹𐌸 {{PLURAL:$1|𐌼𐌹𐌽𐌽𐌹𐌶𐍉𐌽 𐌺𐌿𐌽𐌹|𐌹𐍆𐍄𐌿𐌼𐌰 $1 𐌼𐌹𐌽𐌽𐌹𐌶𐍉𐌽𐌰 𐌺𐌿𐌽𐌾𐌰}}, 𐌰𐌻𐌻𐌰𐌹𐌶𐌴 $2 𐌺𐌿𐌽𐌾𐌴.}}",
        "category-subcat-count-limited": "𐌸𐌰𐍄𐌰 𐌺𐌿𐌽𐌹 𐌷𐌰𐌱𐌰𐌹𐌸 𐌹𐍆𐍄𐌿𐌼𐍉𐌽/𐌹𐍆𐍄𐌿𐌼𐍉𐌽𐌰\n{{PLURAL:$1|𐌼𐌹𐌽𐌽𐌹𐌶𐍉𐌽 𐌺𐌿𐌽𐌹|$1 𐌼𐌹𐌽𐌽𐌹𐌶𐍉𐌽𐌰 𐌺𐌿𐌽𐌾𐌰}}.",
        "category-article-count": "{{PLURAL:$2|𐌸𐌰𐍄𐌰 𐌺𐌿𐌽𐌹 𐌷𐌰𐌱𐌰𐌹𐌸 𐌸𐌰𐍄𐌰𐌹𐌽𐌴𐌹 𐌹𐍆𐍄𐌿𐌼𐌰𐌽 𐌻𐌰𐌿𐍆.|𐌹𐍆𐍄𐌿𐌼𐌰(𐌽𐍃) {{PLURAL:$1|𐌻𐌰𐌿𐍆𐍃 𐌹𐍃𐍄|$1 𐌻𐌰𐌿𐌱𐍉𐍃 𐍃𐌹𐌽𐌳}} 𐌹𐌽 𐌸𐌰𐌼𐌼𐌰 𐌺𐌿𐌽𐌾𐌰, 𐌰𐌻𐌻𐌰𐌹𐌶𐌴 $2 𐌻𐌰𐌿𐌱𐌴.}}",
-       "category-article-count-limited": "{{{{PLURAL:$1|𐌹𐍆𐍄𐌿𐌼𐍃 𐌻𐌰𐌿𐍆𐍃 𐌹𐍃𐍄|$1 𐌹𐍆𐍄𐌿𐌼𐌰𐌹 𐌻𐌰𐌿𐌱𐍉𐍃 𐍃𐌹𐌽𐌳}} 𐌹𐌽 𐌰𐌽𐌳𐍅𐌰𐌹𐍂𐌸𐌰𐌼𐌼𐌰 𐌺𐌿𐌽𐌾𐌰.",
+       "category-article-count-limited": "{{PLURAL:$1|𐌹𐍆𐍄𐌿𐌼𐍃 𐌻𐌰𐌿𐍆𐍃 𐌹𐍃𐍄|$1 𐌹𐍆𐍄𐌿𐌼𐌰𐌹 𐌻𐌰𐌿𐌱𐍉𐍃 𐍃𐌹𐌽𐌳}} 𐌹𐌽 𐌰𐌽𐌳𐍅𐌰𐌹𐍂𐌸𐌰𐌼𐌼𐌰 𐌺𐌿𐌽𐌾𐌰.",
        "category-file-count": "{{PLURAL:$2|𐌸𐌰𐍄𐌰 𐌺𐌿𐌽𐌹 𐌷𐌰𐌱𐌰𐌹𐌸 𐌸𐌰𐍄𐌰𐌹𐌽𐌴𐌹 𐌹𐍆𐍄𐌿𐌼 𐍆𐌰𐌾𐌻.|𐌹𐍆𐍄𐌿𐌼𐍉(𐌽𐌰) {{PLURAL:$1|𐍆𐌰𐌾𐌻 𐌹𐍃𐍄|$1 𐍆𐌰𐌾𐌻𐌰 𐍃𐌹𐌽𐌳}} 𐌹𐌽 𐌸𐌰𐌼𐌼𐌰 𐌺𐌿𐌽𐌾𐌰, 𐌰𐌻𐌻𐌰𐌹𐌶𐌴 $2 𐌻𐌰𐌿𐌱𐌴.}}",
        "broken-file-category": "𐌻𐌰𐌿𐌱𐍉𐍃 𐌼𐌹𐌸 𐌱𐍂𐌹𐌺𐌰𐌽𐌰𐌹𐌼 𐍆𐌰𐌾𐌻𐌰𐌲𐌰𐍅𐌹𐍃𐍃𐌹𐌼",
        "about": "𐌱𐌹",
        "talk": "𐌲𐌰𐍅𐌰𐌿𐍂𐌳𐌹",
        "views": "𐍃𐌹𐌿𐌽𐌴𐌹𐍃",
        "toolbox": "𐍃𐌰𐍂𐍅𐌰𐌽𐍃",
-       "tool-link-emailuser": "𐍃𐌰𐌽𐌳𐌴𐌹 𐌴-𐌱𐍉𐌺𐍉𐍃 𐌳𐌿 {{{{GENDER:$1|𐌼𐌰𐌽𐌽}}",
+       "tool-link-emailuser": "𐍃𐌰𐌽𐌳𐌴𐌹 𐌴-𐌱𐍉𐌺𐍉𐍃 𐌳𐌿 {{GENDER:$1|𐌼𐌰𐌽𐌽}}",
        "imagepage": "𐌱𐌰𐌽𐌳𐍅𐌴𐌹 𐍆𐌰𐌾𐌻𐌰𐌻𐌰𐌿𐍆",
        "mediawikipage": "𐌰𐌽𐌳𐌷𐌿𐌻𐌴𐌹 𐍅𐌰𐌿𐍂𐌳𐌰𐌻𐌰𐌿𐍆",
        "viewhelppage": "𐌰𐌽𐌳𐌷𐌿𐌻𐌴𐌹 𐌷𐌹𐌻𐍀𐌰𐌻𐌰𐌿𐍆",
        "cannotlogoutnow-title": "𐌰𐍆𐌻𐌴𐌹𐌸𐌰𐌽 𐌽𐌿 𐌼𐌰𐌷𐍄𐍃 𐌽𐌹𐍃𐍄",
        "cannotlogoutnow-text": "𐌸𐌰𐌽 $1 𐌱𐍂𐌿𐌺𐌾𐌰𐌳𐌰 𐌰𐍆𐌻𐌴𐌹𐌸𐌰𐌽 𐌼𐌰𐌷𐍄𐍃 𐌽𐌹𐍃𐍄.",
        "welcomeuser": "𐍅𐌰𐌹𐌻𐌰 𐌰𐌽𐌳𐌰𐌽𐌴𐌼𐍃, $1!",
+       "welcomecreation-msg": "𐌸𐌴𐌹𐌽𐌰 𐍂𐌰𐌷𐌽𐌴𐌹𐌽𐍃 𐌲𐌰𐍃𐌺𐍀𐌰𐌽𐌰 𐌹𐍃𐍄.\n𐌼𐌰𐌲𐍄 {{SITENAME}} [[Special:Preferences|𐍆𐍂𐌹𐌾𐍉𐌽𐍉𐍃 𐍅𐌰𐌹𐌷𐍄𐌹𐌽𐍃]] 𐌹𐌽𐌼𐌰𐌹𐌳𐌾𐌰𐌽 𐌾𐌰𐌱𐌰𐌹 𐍅𐌹𐌻𐌴𐌹𐍃.",
        "yourname": "𐌰𐍄𐌲𐌰𐌲𐌲𐌰𐌽𐌰𐌼𐍉:",
        "userlogin-yourname": "𐌰𐍄𐌲𐌰𐌲𐌲𐌰𐌽𐌰𐌼𐍉",
        "userlogin-yourname-ph": "𐌼𐌴𐌻𐌴𐌹 𐌰𐍄𐌲𐌰𐌲𐌲𐌰𐌽𐌰𐌼𐍉 𐌸𐌴𐌹𐌽",
        "cannotlogin-text": "𐌰𐍄𐌲𐌰𐌲𐌲𐌰𐌽 𐌼𐌰𐌷𐍄𐍃 𐌽𐌹𐍃𐍄.",
        "cannotloginnow-title": "𐌽𐌿 𐌽𐌹 𐌼𐌰𐌲𐍄 𐌰𐍄𐌲𐌰𐌲𐌲𐌰𐌽",
        "cannotloginnow-text": "𐌰𐍄𐌲𐌰𐌲𐌲𐌰𐌽 𐌼𐌰𐌷𐍄𐍃 𐌽𐌹𐍃𐍄 𐌸𐌰𐌽 $1 𐌱𐍂𐌿𐌺𐌾𐌰𐌳𐌰.",
+       "cannotcreateaccount-title": "𐌼𐌰𐌷𐍄𐍃 𐌽𐌹 𐍅𐌰𐍃 𐌳𐌿 𐍃𐌺𐌰𐍀𐌾𐌰𐌽 𐍂𐌰𐌷𐌽𐌴𐌹𐌽𐌹𐌽𐍃",
        "password-change-forbidden": "𐌽𐌹 𐌼𐌰𐌲𐍄 𐌹𐌽𐌼𐌰𐌹𐌳𐌾𐌰𐌽 𐌲𐌰𐌼𐍉𐍄𐌰𐍅𐌰𐌿𐍂𐌳𐌰 𐌰𐌽𐌰 𐌸𐌰𐌼𐌼𐌰 𐍅𐌹𐌺𐌾𐌰.",
        "login": "𐌰𐍄𐌲𐌰𐌲𐌲",
-       "nav-login-createaccount": "ð\90\8c°ð\90\8d\84ð\90\8c²ð\90\8c°ð\90\8c²ð\90\8c² / ð\90\8d\83ð\90\8cºð\90\8c°ð\90\8d\80ð\90\8c´ð\90\8c¹ ð\90\8cºð\90\8c°ð\90\8d\85ð\90\8d\84ð\90\8d\83ð\90\8c¾ð\90\8d\89𐌽",
+       "nav-login-createaccount": "ð\90\8c°ð\90\8d\84ð\90\8c²ð\90\8c°ð\90\8c²ð\90\8c² / ð\90\8d\83ð\90\8cºð\90\8c°ð\90\8d\80ð\90\8c´ð\90\8c¹ ð\90\8d\82ð\90\8c°ð\90\8c·ð\90\8c½ð\90\8c´ð\90\8c¹𐌽",
        "logout": "𐌰𐍆𐌻𐌴𐌹𐌸",
        "userlogout": "𐌰𐍆𐌻𐌴𐌹𐌸",
-       "userlogin-noaccount": "ð\90\8c½ð\90\8c¹ ð\90\8c·ð\90\8c°ð\90\8c±ð\90\8c°ð\90\8c¹ð\90\8d\83 ð\90\8cºð\90\8c°ð\90\8d\85ð\90\8d\84ð\90\8d\83ð\90\8c¾ð\90\8d\89𐌽?",
+       "userlogin-noaccount": "ð\90\8c½ð\90\8c¹ ð\90\8c·ð\90\8c°ð\90\8c±ð\90\8c°ð\90\8c¹ð\90\8d\83 ð\90\8d\82ð\90\8c°ð\90\8c·ð\90\8c½ð\90\8c´ð\90\8c¹𐌽?",
        "userlogin-joinproject": "𐌲𐌰𐌳𐌰𐌹𐌻𐌴𐌹 𐌹𐌽 𐌽𐌰𐍄𐌾𐌰𐍃𐍄𐌰𐌳𐌰 {{SITENAME}}",
-       "createaccount": "ð\90\8d\83ð\90\8cºð\90\8c°ð\90\8d\80ð\90\8c´ð\90\8c¹ ð\90\8cºð\90\8c°ð\90\8d\85ð\90\8d\84ð\90\8d\83ð\90\8c¾ð\90\8d\89𐌽",
+       "createaccount": "ð\90\8d\83ð\90\8cºð\90\8c°ð\90\8d\80ð\90\8c´ð\90\8c¹ ð\90\8d\82ð\90\8c°ð\90\8c·ð\90\8c½ð\90\8c´ð\90\8c¹𐌽",
        "userlogin-resetpassword-link": "𐌿𐍆𐌰𐍂𐌼𐌿𐌽𐌽𐍉𐌳𐌴𐍃 𐌸𐌴𐌹𐌽𐌰𐌼𐌼𐌰 𐌲𐌰𐌼𐍉𐍄𐌰𐍅𐌰𐌿𐍂𐌳𐌰?",
        "userlogin-helplink2": "𐌷𐌹𐌻𐍀𐌰 𐌼𐌹𐌸 𐌰𐍄𐌲𐌰𐌲𐌲𐌰",
+       "userlogin-createanother": "𐍃𐌺𐌰𐍀𐌴𐌹 𐌰𐌽𐌸𐌰𐍂𐌰 𐍂𐌰𐌷𐌽𐌴𐌹𐌽",
        "createacct-emailoptional": "𐌴-𐌱𐍉𐌺𐍉𐍃 (𐌼𐌰𐌷𐍄𐌴𐌹𐌲𐍉𐍃)",
        "createacct-email-ph": "𐌼𐌴𐌻𐌴𐌹 𐌸𐌴𐌹𐌽𐍉𐍃 𐌴-𐌱𐍉𐌺𐍉𐍃",
        "createacct-another-email-ph": "𐌼𐌴𐌻𐌴𐌹 𐌴-𐌱𐍉𐌺𐍉𐍃",
+       "createaccountmail-help": "𐌼𐌰𐌲 𐌱𐍂𐌿𐌺𐌾𐌰𐌳𐌰 𐌳𐌿 𐍃𐌺𐌰𐍀𐌾𐌰𐌽 𐍂𐌰𐌷𐌽𐌴𐌹𐌽 𐌰𐌽𐌸𐌰𐍂𐌰𐌼𐌼𐌰 𐌼𐌰𐌽 𐌹𐌽𐌿𐌷 𐌲𐌰𐌼𐍉𐍄𐌰𐍅𐌰𐌿𐍂𐌳 𐌻𐌰𐌹𐍃𐌾𐌰𐌽",
        "createacct-realname": "𐍃𐌿𐌽𐌾𐌴𐌹𐌽 𐌽𐌰𐌼𐍉 (𐌼𐌰𐌷𐍄𐌴𐌹𐌲)",
        "createacct-reason": "𐌲𐍂𐌿𐌽𐌳𐌿𐍃",
-       "createacct-submit": "𐍃𐌺𐌰𐍀𐌴𐌹 𐌸𐌴𐌹𐌽𐌰 𐌺𐌰𐍅𐍄𐍃𐌾𐍉𐌽",
+       "createacct-reason-ph": "𐌳𐌿𐍈𐌴 𐌸𐌿 𐍃𐌺𐌰𐍀𐌾𐌹𐍃 𐍂𐌰𐌷𐌽𐌴𐌹𐌽",
+       "createacct-submit": "𐍃𐌺𐌰𐍀𐌴𐌹 𐌸𐌴𐌹𐌽𐌰 𐍂𐌰𐌷𐌽𐌴𐌹𐌽",
+       "createacct-another-submit": "𐍃𐌺𐌰𐍀𐌴𐌹 𐍂𐌰𐌷𐌽𐌴𐌹𐌽",
+       "createacct-another-continue-submit": "𐌸𐌰𐌹𐍂𐌷𐍅𐌹𐍃 𐌼𐌹𐌸 𐌲𐌰𐍃𐌺𐌰𐍆𐍄𐌰𐌹 𐍂𐌰𐌷𐌽𐌴𐌹𐌽𐌰𐌹𐍃",
        "createacct-benefit-heading": "{{SITENAME}} 𐍄𐌰𐍅𐌹𐌸 𐌹𐍃𐍄 𐍆𐍂𐌰𐌼 𐌼𐌰𐌽𐌽𐌰𐌼 𐍃𐍅𐌴 𐌸𐌿𐌺.",
        "createacct-benefit-body1": "{{PLURAL:$1|𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹𐌽𐍃|𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹𐌽𐍉𐍃}}",
        "createacct-benefit-body2": "{{PLURAL:$1|𐌻𐌰𐌿𐍆𐍃|𐌻𐌰𐌿𐌱𐍉𐍃}}",
        "badretype": "𐌲𐌰𐌼𐍉𐍄𐌰𐍅𐌰𐌿𐍂𐌳𐌰 𐌸𐍉𐌴𐌹 𐌸𐌿 𐌲𐌰𐌼𐌴𐌻𐌹𐌳𐌴𐍃 𐌽𐌹 𐌹𐌽𐌲𐌰𐌻𐌴𐌹𐌺𐍉𐌽𐌳.",
+       "usernameinprogress": "𐍂𐌰𐌷𐌽𐌴𐌹𐌽𐌹𐌲𐌰𐍃𐌺𐌰𐍆𐍄𐍃 𐌸𐌰𐌼𐌼𐌰 𐌰𐍄𐌲𐌰𐌲𐌲𐌰𐌽𐌰𐌼𐌹𐌽 𐌾𐌿 𐍄𐍉𐌾𐌰𐌳𐌰. 𐌱𐌹𐌳𐌾𐌰𐌼 𐌸𐌿𐌺 𐌴𐌹 𐌱𐌹𐌳𐌰𐌹𐍃.",
+       "createacct-error": "𐌰𐌹𐍂𐌶𐌴𐌹 𐌹𐌽 𐌲𐌰𐍃𐌺𐌰𐍆𐍄𐌰𐌹 𐍂𐌰𐌷𐌽𐌴𐌹𐌽𐌰𐌹𐍃",
+       "createaccounterror": "𐌼𐌰𐌷𐍄𐍃 𐌽𐌹 𐍅𐌰𐍃 𐌳𐌿 𐍃𐌺𐌰𐍀𐌾𐌰𐌽 𐍂𐌰𐌷𐌽𐌴𐌹𐌽: $1",
+       "nocookiesfornew": "𐌱𐍂𐌿𐌺𐌾𐌰𐍂𐌰𐌷𐌽𐌴𐌹𐌽𐍃 𐌽𐌹 𐌲𐌰𐍃𐌺𐌰𐍀𐌰𐌽𐌰 𐌿𐌽𐍄𐌴 𐍅𐌴𐌹𐍃 𐌽𐌹 𐌼𐌰𐌷𐍄𐌴𐌳𐌿𐌼 𐌱𐍂𐌿𐌽𐌽𐌰𐌽 𐌲𐌰𐍃𐌹𐌲𐌻𐌾𐌰𐌽. 𐍅𐌹𐍃 𐌰𐍂𐌽𐌹𐌱𐌰 𐌸𐌰𐍄𐌴𐌹 𐌸𐌿 𐌰𐌽𐌳𐌻𐌴𐍄𐌹𐍃 𐌺𐍉𐌺𐍉𐍃, 𐌰𐍆𐍄𐍂𐌰𐌰𐌽𐌰𐌽𐌹𐌿𐌴𐌹 𐌻𐌰𐌿𐍆 𐌾𐌰𐌷 𐍃𐍉𐌺𐌴𐌹 𐌰𐍆𐍄𐍂𐌰.",
+       "nosuchuser": "𐌽𐌹𐍃𐍄 𐌱𐍂𐌿𐌺𐌾𐌰𐌽𐌳𐍃 𐌼𐌹𐌸 𐌽𐌰𐌼𐌹𐌽 \"$1\".\n𐌽𐌰𐌼𐌽𐌰 𐌱𐍂𐌿𐌺𐌾𐌰𐌽𐌳𐌴 𐌼𐌰𐌲𐌿𐌽 𐌷𐌰𐌱𐌰𐌽 𐌷𐌰𐌿𐌱𐌹𐌳𐌹𐍃 𐌱𐍉𐌺𐍉𐍃.\n𐍃𐌰𐌹𐍈 𐌰𐍆𐍄𐍂𐌰 𐌸𐌴𐌹𐌽𐌰 𐌼𐌴𐌻𐌴𐌹𐌽𐍃, 𐌸𐌰𐌿 [[Special:CreateAccount|𐍃𐌺𐌰𐍀𐌴𐌹 𐍂𐌰𐌷𐌽𐌴𐌹𐌽]].",
        "nouserspecified": "𐍃𐌺𐌰𐌻𐍄 𐌲𐌹𐌱𐌰𐌽 𐌰𐍄𐌲𐌰𐌲𐌲𐌰𐌽𐌰𐌼𐍉.",
        "password-login-forbidden": "𐌱𐍂𐌿𐌺𐌴𐌹𐌽𐍃 𐌸𐌹𐍃 𐌰𐍄𐌲𐌰𐌲𐌲𐌰𐌽𐌰𐌼𐌹𐌽𐍃 𐌾𐌰𐌷 𐌸𐌹𐍃 𐌲𐌰𐌼𐍉𐍄𐌰𐍅𐌰𐌿𐍂𐌳𐌹𐍃 𐍆𐌰𐌿𐍂𐌱𐌹𐌿𐌳𐌰𐌽𐌰 𐌹𐍃𐍄.",
        "mailmypassword": "𐌰𐍆𐍄𐍂𐌰 𐍃𐌰𐍄𐌴𐌹 𐌲𐌰𐌼𐍉𐍄𐌰𐍅𐌰𐌿𐍂𐌳",
        "emailconfirmlink": "𐌲𐌰𐍃𐌹𐌲𐌻𐌴𐌹 𐌸𐌴𐌹𐌽𐍉𐍃 𐌴-𐌱𐍉𐌺𐍉𐍃",
        "emaildisabled": "𐍃𐌰 𐌽𐌰𐍄𐌾𐌰𐍃𐍄𐌰𐌸𐍃 𐌽𐌹 𐌼𐌰𐌲 𐍃𐌰𐌽𐌳𐌾𐌰𐌽 𐌴-𐌱𐍉𐌺𐍉𐍃.",
+       "accountcreatedtext": "𐌱𐍂𐌿𐌺𐌾𐌰𐍂𐌰𐌷𐌽𐌴𐌹𐌽𐍃 [[{{ns:User}}:$1|$1]] ([[{{ns:User talk}}:$1|𐌲𐌰𐍅𐌰𐌿𐍂𐌳𐌹]]) 𐌲𐌰𐍃𐌺𐌰𐍀𐌰𐌽𐌰 𐌹𐍃𐍄.",
        "loginlanguagelabel": "𐍂𐌰𐌶𐌳𐌰: $1",
        "pt-login": "𐌰𐍄𐌲𐌰𐌲𐌲",
        "pt-login-button": "𐌰𐍄𐌲𐌰𐌲𐌲",
        "pt-login-continue-button": "𐌸𐌰𐌹𐍂𐌷𐍅𐌹𐍃 𐌰𐍄𐌲𐌰𐌲𐌲𐌰𐌽",
-       "pt-createaccount": "ð\90\8d\83ð\90\8cºð\90\8c°ð\90\8d\80ð\90\8c´ð\90\8c¹ ð\90\8cºð\90\8c°ð\90\8d\85ð\90\8d\84ð\90\8d\83ð\90\8c¾ð\90\8d\89𐌽",
+       "pt-createaccount": "ð\90\8d\83ð\90\8cºð\90\8c°ð\90\8d\80ð\90\8c´ð\90\8c¹ ð\90\8d\82ð\90\8c°ð\90\8c·ð\90\8c½ð\90\8c´ð\90\8c¹𐌽",
        "pt-userlogout": "𐌰𐍆𐌻𐌴𐌹𐌸",
+       "resetpass_header": "𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹 𐌲𐌰𐌼𐍉𐍄𐌰𐍅𐌰𐌿𐍂𐌳 𐍂𐌰𐌷𐌽𐌴𐌹𐌽𐌰𐌹𐍃",
        "oldpassword": "𐍆𐌰𐌹𐍂𐌽𐌹 𐌲𐌰𐌼𐍉𐍄𐌰𐍅𐌰𐌿𐍂𐌳:",
        "botpasswords": "𐌱𐌰𐌿𐍄𐌹𐍃 𐌲𐌰𐌼𐍉𐍄𐌰𐍅𐌰𐌿𐍂𐌳𐌰",
        "botpasswords-existing": "𐍅𐌹𐍃𐌰𐌽𐌳𐍉𐌽𐌳𐌰 𐌱𐌰𐌿𐍄𐌹𐍃 𐌲𐌰𐌼𐍉𐍄𐌰𐍅𐌰𐌿𐍂𐌳𐌰",
        "loginreqlink": "𐌰𐍄𐌲𐌰𐌲𐌲",
        "newarticle": "(𐌽𐌹𐍅𐌹)",
        "newarticletext": "𐌻𐌰𐌹𐍃𐍄𐌹𐌳𐌴𐍃 𐌲𐌰𐍅𐌹𐍃 𐌳𐌿 𐌻𐌰𐌿𐌱𐌰 𐍃𐌰𐌴𐌹 𐌽𐌹𐍃𐍄. 𐌳𐌿 𐍃𐌺𐌰𐍀𐌾𐌰𐌽 𐌸𐌰𐌽𐌰 𐌻𐌰𐌿𐍆, 𐌰𐌽 𐌰𐍃𐍄𐍉𐌳𐌴𐌹 𐌼𐌴𐌻𐌾𐌰𐌽 𐌹𐌽 𐌰𐍂𐌺𐌰𐌹 𐌿𐍆 (𐍃𐌰𐌹𐍈 [$1 𐌷𐌹𐌻𐍀𐌰𐌻𐌰𐌿𐍆] 𐌼𐌰𐌽𐌰𐌲𐌹𐌶𐌹𐌽 𐌺𐌿𐌽𐌸𐌾𐌰). 𐌾𐌰𐌱𐌰𐌹 𐌹𐍃 𐌷𐌴𐍂 𐌹𐌽 𐌰𐌹𐍂𐌶𐌴𐌹𐌽𐍃, 𐌲𐌰𐌲𐌲 𐌳𐌿 <strong>𐌹𐌱𐌿𐌺𐌰𐌷𐌰𐌿𐌱𐌹𐌳𐌹𐌻𐍉𐌽</strong>.",
-       "anontalkpagetext": "----\n<em>ð\90\8c¸ð\90\8c°ð\90\8d\84ð\90\8c° ð\90\8c¹ð\90\8d\83ð\90\8d\84 ð\90\8c²ð\90\8c°ð\90\8d\85ð\90\8c°ð\90\8c¿ð\90\8d\82ð\90\8c³ð\90\8c¾ð\90\8c°ð\90\8c»ð\90\8c°ð\90\8c¿ð\90\8d\86ð\90\8d\83 ð\90\8c¿ð\90\8c½ð\90\8c½ð\90\8c°ð\90\8c¼ð\90\8c½ð\90\8c¹ð\90\8c¸ð\90\8c°ð\90\8c¼ð\90\8c¼ð\90\8c° ð\90\8c±ð\90\8d\82ð\90\8c¿ð\90\8cºð\90\8c¾ð\90\8c°ð\90\8c½ð\90\8c³ ð\90\8d\83ð\90\8c°ð\90\8c´ð\90\8c¹ ð\90\8c½ð\90\8c°ð\90\8c¿ð\90\8c· ð\90\8c½ð\90\8c¹ ð\90\8c²ð\90\8c°ð\90\8d\83ð\90\8cºð\90\8d\89ð\90\8d\80 ð\90\8c°ð\90\8cºð\90\8c°ð\90\8d\85ð\90\8c½ð\90\8d\84, ð\90\8c¸ð\90\8c°ð\90\8c¿ ð\90\8d\83ð\90\8c°ð\90\8c´ð\90\8c¹ ð\90\8c½ð\90\8c¹ ð\90\8c±ð\90\8d\82ð\90\8c¿ð\90\8cºð\90\8c¾ð\90\8c¹ð\90\8c¸ ð\90\8c¸ð\90\8c¹ð\90\8d\83.<em>\nð\90\8c¹ð\90\8c½ ð\90\8c¸ð\90\8c¹ð\90\8d\83 ð\90\8d\83ð\90\8cºð\90\8c¿ð\90\8c»ð\90\8c³ ð\90\8c¹ð\90\8d\83ð\90\8d\84 ð\90\8c¿ð\90\8c½ð\90\8d\83 ð\90\8c³ð\90\8c¿ ð\90\8c±ð\90\8d\82ð\90\8c¿ð\90\8cºð\90\8c¾ð\90\8c°ð\90\8c½ ð\90\8d\82ð\90\8c°ð\90\8c¸ð\90\8c¾ð\90\8d\89ð\90\8c½ð\90\8d\83 IP ð\90\8c³ð\90\8c¿ ð\90\8cºð\90\8c¿ð\90\8c½ð\90\8c½ð\90\8c°ð\90\8c½ ð\90\8c¹ð\90\8c½ð\90\8c°/ð\90\8c¹ð\90\8c¾ð\90\8c°.\nð\90\8c¾ð\90\8c°ð\90\8c±ð\90\8c°ð\90\8c¹ ð\90\8c¸ð\90\8c¿ ð\90\8c¹ð\90\8d\83 ð\90\8c¿ð\90\8c½ð\90\8c½ð\90\8c°ð\90\8c¼ð\90\8c½ð\90\8c¹ð\90\8c¸ð\90\8d\83 ð\90\8c±ð\90\8d\82ð\90\8c¿ð\90\8cºð\90\8c¾ð\90\8c°ð\90\8c½ð\90\8c³ð\90\8d\83 ð\90\8c¾ð\90\8c°ð\90\8c· ð\90\8c¹ð\90\8d\83ð\90\8d\84 ð\90\8c¸ð\90\8c¿ð\90\8d\83 ð\90\8d\83ð\90\8d\85ð\90\8c°ð\90\8d\83ð\90\8d\85ð\90\8c´ ð\90\8c¿ð\90\8c½ð\90\8c²ð\90\8c°ð\90\8c·ð\90\8c°ð\90\8c·ð\90\8c¾ð\90\8d\89 ð\90\8d\85ð\90\8c°ð\90\8c¿ð\90\8d\82ð\90\8c³ð\90\8c° ð\90\8d\85ð\90\8c´ð\90\8d\83ð\90\8c¿ð\90\8c½ ð\90\8c¸ð\90\8c¿ð\90\8d\83, ð\90\8c±ð\90\8c¹ð\90\8c³ð\90\8c¾ð\90\8c°ð\90\8c¼ ð\90\8c¸ð\90\8c¿ð\90\8cº, [[Special:CreateAccount|ð\90\8d\83ð\90\8cºð\90\8c°ð\90\8d\80ð\90\8c´ð\90\8c¹ ð\90\8c°ð\90\8cºð\90\8c°ð\90\8d\85ð\90\8c½ð\90\8d\84]] 𐌸𐌰𐌿 [[Special:UserLogin|𐌰𐍄𐌲𐌰𐌲𐌲]] 𐌴𐌹 𐌽𐌹 𐍃𐌹𐌾𐌰𐌹 𐌰𐌹𐍂𐌶𐌴𐌹𐌽𐍃 𐌼𐌹𐌸 𐌰𐌽𐌸𐌰𐍂𐌰𐌹𐌼 𐌿𐌽𐌽𐌰𐌼𐌽𐌹𐌸𐌰𐌹𐌼 𐌱𐍂𐌿𐌺𐌾𐌰𐌽𐌳𐌰𐌼 𐌹𐌽 𐌰𐌽𐌰𐍅𐌰𐌹𐍂𐌸𐌰.",
+       "anontalkpagetext": "----\n<em>ð\90\8c¸ð\90\8c°ð\90\8d\84ð\90\8c° ð\90\8c¹ð\90\8d\83ð\90\8d\84 ð\90\8c²ð\90\8c°ð\90\8d\85ð\90\8c°ð\90\8c¿ð\90\8d\82ð\90\8c³ð\90\8c¾ð\90\8c°ð\90\8c»ð\90\8c°ð\90\8c¿ð\90\8d\86ð\90\8d\83 ð\90\8c¿ð\90\8c½ð\90\8c½ð\90\8c°ð\90\8c¼ð\90\8c½ð\90\8c¹ð\90\8c¸ð\90\8c°ð\90\8c¼ð\90\8c¼ð\90\8c° ð\90\8c±ð\90\8d\82ð\90\8c¿ð\90\8cºð\90\8c¾ð\90\8c°ð\90\8c½ð\90\8c³ ð\90\8d\83ð\90\8c°ð\90\8c´ð\90\8c¹ ð\90\8c½ð\90\8c°ð\90\8c¿ð\90\8c· ð\90\8c½ð\90\8c¹ ð\90\8c²ð\90\8c°ð\90\8d\83ð\90\8cºð\90\8d\89ð\90\8d\80 ð\90\8d\82ð\90\8c°ð\90\8c·ð\90\8c½ð\90\8c´ð\90\8c¹ð\90\8c½, ð\90\8c¸ð\90\8c°ð\90\8c¿ ð\90\8d\83ð\90\8c°ð\90\8c´ð\90\8c¹ ð\90\8c½ð\90\8c¹ ð\90\8c±ð\90\8d\82ð\90\8c¿ð\90\8cºð\90\8c¾ð\90\8c¹ð\90\8c¸ ð\90\8c¸ð\90\8c¹ð\90\8d\83.<em>\nð\90\8c¹ð\90\8c½ ð\90\8c¸ð\90\8c¹ð\90\8d\83 ð\90\8d\83ð\90\8cºð\90\8c¿ð\90\8c»ð\90\8c³ ð\90\8c¹ð\90\8d\83ð\90\8d\84 ð\90\8c¿ð\90\8c½ð\90\8d\83 ð\90\8c³ð\90\8c¿ ð\90\8c±ð\90\8d\82ð\90\8c¿ð\90\8cºð\90\8c¾ð\90\8c°ð\90\8c½ ð\90\8d\82ð\90\8c°ð\90\8c¸ð\90\8c¾ð\90\8d\89ð\90\8c½ð\90\8d\83 IP ð\90\8c³ð\90\8c¿ ð\90\8cºð\90\8c¿ð\90\8c½ð\90\8c½ð\90\8c°ð\90\8c½ ð\90\8c¹ð\90\8c½ð\90\8c°/ð\90\8c¹ð\90\8c¾ð\90\8c°.\nð\90\8c¾ð\90\8c°ð\90\8c±ð\90\8c°ð\90\8c¹ ð\90\8c¸ð\90\8c¿ ð\90\8c¹ð\90\8d\83 ð\90\8c¿ð\90\8c½ð\90\8c½ð\90\8c°ð\90\8c¼ð\90\8c½ð\90\8c¹ð\90\8c¸ð\90\8d\83 ð\90\8c±ð\90\8d\82ð\90\8c¿ð\90\8cºð\90\8c¾ð\90\8c°ð\90\8c½ð\90\8c³ð\90\8d\83 ð\90\8c¾ð\90\8c°ð\90\8c· ð\90\8c¹ð\90\8d\83ð\90\8d\84 ð\90\8c¸ð\90\8c¿ð\90\8d\83 ð\90\8d\83ð\90\8d\85ð\90\8c°ð\90\8d\83ð\90\8d\85ð\90\8c´ ð\90\8c¿ð\90\8c½ð\90\8c²ð\90\8c°ð\90\8c·ð\90\8c°ð\90\8c·ð\90\8c¾ð\90\8d\89 ð\90\8d\85ð\90\8c°ð\90\8c¿ð\90\8d\82ð\90\8c³ð\90\8c° ð\90\8d\85ð\90\8c´ð\90\8d\83ð\90\8c¿ð\90\8c½ ð\90\8c¸ð\90\8c¿ð\90\8d\83, ð\90\8c±ð\90\8c¹ð\90\8c³ð\90\8c¾ð\90\8c°ð\90\8c¼ ð\90\8c¸ð\90\8c¿ð\90\8cº, [[Special:CreateAccount|ð\90\8d\83ð\90\8cºð\90\8c°ð\90\8d\80ð\90\8c´ð\90\8c¹ ð\90\8d\82ð\90\8c°ð\90\8c·ð\90\8c½ð\90\8c´ð\90\8c¹ð\90\8c½]] 𐌸𐌰𐌿 [[Special:UserLogin|𐌰𐍄𐌲𐌰𐌲𐌲]] 𐌴𐌹 𐌽𐌹 𐍃𐌹𐌾𐌰𐌹 𐌰𐌹𐍂𐌶𐌴𐌹𐌽𐍃 𐌼𐌹𐌸 𐌰𐌽𐌸𐌰𐍂𐌰𐌹𐌼 𐌿𐌽𐌽𐌰𐌼𐌽𐌹𐌸𐌰𐌹𐌼 𐌱𐍂𐌿𐌺𐌾𐌰𐌽𐌳𐌰𐌼 𐌹𐌽 𐌰𐌽𐌰𐍅𐌰𐌹𐍂𐌸𐌰.",
        "noarticletext": "𐌽𐌿 𐌽𐌹 𐍃𐌹𐌽𐌳 𐌱𐍉𐌺𐍉𐍃 𐌹𐌽 𐌸𐌰𐌼𐌼𐌰 𐌻𐌰𐌿𐌱𐌰.\n𐌼𐌰𐌲𐍄 [[Special:Search/{{PAGENAME}}|𐍃𐍉𐌺𐌾𐌰𐌽 𐌸𐌰𐍄𐌰 𐌻𐌰𐌿𐌱𐌰-𐌿𐍆𐌰𐍂𐌼𐌴𐌻𐌹]] 𐌹𐌽 𐌰𐌽𐌸𐌰𐍂𐌰𐌹𐌼 𐌻𐌰𐌿𐌱𐌰𐌼,  <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} 𐍃𐍉𐌺𐌾𐌰𐌽 𐌲𐌰𐌷𐌰𐌷𐌾𐍉 𐌲𐌰𐍆𐌰𐍃𐍄𐍉𐍃], 𐌰𐌹𐌸𐌸𐌰𐌿 [{{fullurl:{{FULLPAGENAME}}|action=edit}} 𐍃𐌺𐌰𐍀𐌾𐌰𐌽 𐌸𐌰𐌽𐌰 𐌻𐌰𐌿𐍆.]</ span>",
        "noarticletext-nopermission": "𐌽𐌿 𐌽𐌹 𐍃𐌹𐌽𐌳 𐌱𐍉𐌺𐍉𐍃 𐌹𐌽 𐌸𐌰𐌼𐌼𐌰 𐌻𐌰𐌿𐌱𐌰.\n𐌼𐌰𐌲𐍄 [[Special:Search/{{PAGENAME}}|𐍃𐍉𐌺𐌾𐌰𐌽 𐌸𐌰𐍄𐌰 𐌻𐌰𐌿𐌱𐌰-𐌿𐍆𐌰𐍂𐌼𐌴𐌻𐌹]] 𐌹𐌽 𐌰𐌽𐌸𐌰𐍂𐌰𐌹𐌼 𐌻𐌰𐌿𐌱𐌰𐌼, 𐌸𐌰𐌿 <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} 𐍃𐍉𐌺𐌾𐌰𐌽 𐌲𐌰𐌷𐌰𐌷𐌾𐍉 𐌲𐌰𐍆𐌰𐍃𐍄𐍉𐍃]</span>, 𐌹𐌸 𐌽𐌹 𐌷𐌰𐌱𐌰𐌹𐍃 𐌰𐌽𐌳𐌻𐌴𐍄 𐍃𐌺𐌰𐍀𐌾𐌰𐌽 𐌸𐌰𐌽𐌰 𐌻𐌰𐌿𐍆.",
-       "userpage-userdoesnotexist-view": "ð\90\8c±ð\90\8d\82ð\90\8c¿ð\90\8cºð\90\8c¾ð\90\8c°ð\90\8cºð\90\8c°ð\90\8d\85ð\90\8d\84ð\90\8d\83ð\90\8c¾ð\90\8d\89 \"$1\" 𐌽𐌹𐍃𐍄 𐌰𐌽𐌰𐌼𐌴𐌻𐌹𐌳𐌰.",
+       "userpage-userdoesnotexist-view": "ð\90\8c±ð\90\8d\82ð\90\8c¿ð\90\8cºð\90\8c¾ð\90\8c°ð\90\8d\82ð\90\8c°ð\90\8c·ð\90\8c½ð\90\8c´ð\90\8c¹ð\90\8c½ \"$1\" 𐌽𐌹𐍃𐍄 𐌰𐌽𐌰𐌼𐌴𐌻𐌹𐌳𐌰.",
        "updated": "(𐌰𐌽𐌰𐌽𐌹𐍅𐌹𐌸)",
        "previewnote": "<strong>𐌲𐌰𐌼𐌹𐌽𐌸𐌴𐌹 𐌸𐌰𐍄𐌴𐌹 𐌸𐌰𐍄𐌰 𐌹𐍃𐍄 𐌸𐌰𐍄𐌰𐌹𐌽𐌴𐌹 𐍆𐌰𐌿𐍂𐌰𐍃𐌹𐌿𐌽𐍃.</strong>\n𐌸𐌴𐌹𐌽𐍉𐍃 𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹𐌽𐍉𐍃 𐌽𐌰𐌿𐌷 𐌽𐌹 𐌲𐌰𐍆𐌰𐍃𐍄𐌰𐌽𐍉𐍃 𐍃𐌹𐌽𐌳!",
        "continue-editing": "𐌲𐌰𐌲𐌲 𐌳𐌿 𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹𐌽𐌹𐍃𐍄𐌰𐌳𐌰",
        "shown-title": "𐌱𐌰𐌽𐌳𐍅𐌴𐌹 $1 {{PLURAL:$1|𐍄𐌰𐌿𐌹|𐍄𐍉𐌾𐌰}} 𐍈𐌰𐍂𐌾𐌰𐌼𐌼𐌴𐌷 𐌻𐌰𐌿𐌱𐌰.",
        "viewprevnext": "𐍃𐌹𐌿𐌽𐌴𐌹𐍃 ($1 {{int:pipe-separator}} $2) ($3)",
        "searchmenu-exists": "<strong>𐌹𐍃𐍄 𐌻𐌰𐌿𐍆𐍃 𐌷𐌰𐌹𐍄𐌰𐌽𐍃 \"[[:$1]]\" 𐌰𐌽𐌰 𐌸𐌰𐌼𐌼𐌰 𐍅𐌹𐌺𐌾𐌰.</strong> {{PLURAL:$2|0=|𐍃𐌰𐌹𐍈 𐌾𐌰𐌷 𐌰𐌽𐌸𐌰𐍂𐌰 𐍄𐍉𐌾𐌰 𐍃𐍉𐌺𐌴𐌹𐌽𐌰𐌹𐍃 𐌸𐍉𐌴𐌹 𐌱𐌹𐌲𐌹𐍄𐌰𐌽𐌰 𐍃𐌹𐌽𐌳.}}",
-       "searchmenu-new": "<strong>ð\90\8d\83ð\90\8cºð\90\8c°ð\90\8d\80ð\90\8c´ð\90\8c¹ ð\90\8c»ð\90\8c°ð\90\8c¿ð\90\8d\86 \"[[:$1]]\" ð\90\8c°ð\90\8c½ð\90\8c° ð\90\8c¸ð\90\8c¹ð\90\8c¶ð\90\8c°ð\90\8c¹ ð\90\8d\85ð\90\8c¹ð\90\8cºð\90\8c¹!</strong> {{{{PLURAL:$2|0=|𐍃𐌰𐌹 𐌾𐌰𐌷 𐌻𐌰𐌿𐍆 𐌱𐌹𐌲𐌹𐍄𐌰𐌽𐌰 𐌸𐌴𐌹𐌽𐌰𐌹 𐍃𐍉𐌺𐌴𐌹𐌽𐌰𐌹.|𐍃𐌰𐌹 𐌾𐌰𐌷 𐍄𐍉𐌾𐌰 𐍃𐍉𐌺𐌴𐌹𐌽𐌰𐌹𐍃 𐌱𐌹𐌲𐌹𐍄𐌰𐌽𐌰.}}",
+       "searchmenu-new": "<strong>ð\90\8d\83ð\90\8cºð\90\8c°ð\90\8d\80ð\90\8c´ð\90\8c¹ ð\90\8c»ð\90\8c°ð\90\8c¿ð\90\8d\86 \"[[:$1]]\" ð\90\8c°ð\90\8c½ð\90\8c° ð\90\8c¸ð\90\8c°ð\90\8c¼ð\90\8c¼ð\90\8c° ð\90\8d\85ð\90\8c¹ð\90\8cºð\90\8c¾ð\90\8c°!</strong> {{{{PLURAL:$2|0=|𐍃𐌰𐌹 𐌾𐌰𐌷 𐌻𐌰𐌿𐍆 𐌱𐌹𐌲𐌹𐍄𐌰𐌽𐌰 𐌸𐌴𐌹𐌽𐌰𐌹 𐍃𐍉𐌺𐌴𐌹𐌽𐌰𐌹.|𐍃𐌰𐌹 𐌾𐌰𐌷 𐍄𐍉𐌾𐌰 𐍃𐍉𐌺𐌴𐌹𐌽𐌰𐌹𐍃 𐌱𐌹𐌲𐌹𐍄𐌰𐌽𐌰.}}",
        "searchprofile-articles": "𐌷𐌰𐌱𐌰𐌽𐌳𐌰𐌽𐍃 𐌻𐌰𐌿𐌱𐍉𐍃",
        "searchprofile-images": "𐌼𐌰𐌽𐌰𐌲𐌼𐌰𐌹𐌳𐌾𐌰",
        "searchprofile-everything": "𐌰𐌻𐌻",
        "searchprofile-advanced-tooltip": "𐍃𐍉𐌺𐌴𐌹 𐌹𐌽 𐌱𐌹𐌿𐌷𐍄𐌰𐌹𐌼 𐌽𐌰𐌼𐌰𐍂𐌿𐌼𐌰𐌼",
        "search-result-size": "$1 ({{PLURAL:$2|•𐌰• 𐍅𐌰𐌿𐍂𐌳|•$2• 𐍅𐌰𐌿𐍂𐌳𐌰}})",
        "search-result-category-size": "{{PLURAL:$1|1 𐌲𐌰𐌳𐌰𐌹𐌻𐌰|$1 𐌲𐌰𐌳𐌰𐌹𐌻𐌰𐌽𐍃}} ({{PLURAL:$2|1 𐌼𐌹𐌽𐌽𐌹𐌶𐍉 𐌺𐌿𐌽𐌹|$2 𐌼𐌹𐌽𐌽𐌹𐌶𐍉𐌽𐌰 𐌺𐌿𐌽𐌾𐌰}}, {{PLURAL:$3|1 𐍆𐌰𐌾𐌻|$3 𐍆𐌰𐌾𐌻𐌰}})",
-       "search-redirect": "(ð\90\8c°ð\90\8d\86ð\90\8d\84ð\90\8d\82ð\90\8c°ð\90\8d\85ð\90\8c´ð\90\8c¹ð\90\8d\84ð\90\8d\83 ð\90\8d\86ð\90\8d\82ð\90\8c°ð\90\8c¼ ð\90\8c¸ð\90\8c°ð\90\8c¼ð\90\8c¼ð\90\8c° $1)",
+       "search-redirect": "(ð\90\8c°ð\90\8d\86ð\90\8d\84ð\90\8d\82ð\90\8c°ð\90\8d\84ð\90\8c°ð\90\8c¿ð\90\8c·ð\90\8d\84ð\90\8d\83 ð\90\8d\86ð\90\8d\82ð\90\8c°ð\90\8c¼ $1)",
        "search-section": "(𐍆𐌴𐍂𐌰 $1)",
        "search-suggest": "𐌲𐌰𐌼𐌰𐌽𐍄: $1",
        "searchall": "𐌰𐌻𐌻𐍃",
-       "search-showingresults": "{{ZPLURAL:$4|𐍄𐌰𐌿𐌹 <strong>$1 𐍅𐌰𐌹𐌷𐍄𐌰𐌹𐍃 <strong>$3|𐍄𐍉𐌾𐌰 <strong>$1 - $2 𐍅𐌰𐌹𐌷𐍄𐌰𐌹𐍃 <strong>$3}}",
+       "search-showingresults": "{{PLURAL:$4|𐍄𐌰𐌿𐌹 <strong>$1 𐍅𐌰𐌹𐌷𐍄𐌰𐌹𐍃 <strong>$3|𐍄𐍉𐌾𐌰 <strong>$1 - $2 𐍅𐌰𐌹𐌷𐍄𐌰𐌹𐍃 <strong>$3}}",
        "search-nonefound": "𐌽𐌹 𐍄𐌰𐌿𐌹 𐍅𐌰𐍃 𐍃𐌰𐌼𐌰𐌽𐌰 𐍃𐍅𐌰 𐍃𐍉𐌺𐌴𐌹𐌽.",
        "powersearch-legend": "𐍃𐍉𐌺𐌴𐌹",
        "preferences": "𐌼𐌴𐌹𐌽𐍉𐍃 𐍆𐍂𐌹𐌾𐍉𐌽𐍉𐍃 𐍅𐌰𐌹𐌷𐍄𐍃",
        "recentchanges-label-unpatrolled": "𐍃𐍉 𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹𐌽𐍃 𐌽𐌰𐌿𐌷 𐌽𐌹𐍃𐍄 𐍅𐌰𐍂𐌳𐌹𐌳𐌰",
        "recentchanges-label-plusminus": "𐌻𐌰𐌿𐌱𐌰𐌼𐌹𐌺𐌹𐌻𐌴𐌹 𐌹𐌽𐌼𐌰𐌹𐌳𐌹𐌳𐌰 𐌼𐌹𐌸 𐌸𐌹𐌶𐌰𐌹 𐍂𐌰𐌸𐌾𐍉𐌽 𐌱𐌹𐍄𐍉",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (𐍃𐌰𐌹𐍈 𐌾𐌰𐌷[[Special:NewPages|𐍅𐌹𐌺𐍉 𐌽𐌹𐌿𐌾𐌰𐌹𐌶𐌴 𐌻𐌰𐌿𐌱𐌴]])",
-       "rcfilters-limit-title": "ð\90\8c¹ð\90\8c½ð\90\8c¼ð\90\8c°ð\90\8c¹ð\90\8c³ð\90\8c´ð\90\8c¹ð\90\8c½ð\90\8d\89ð\90\8d\83 𐌳𐌿 𐌱𐌰𐌽𐌳𐍅𐌾𐌰𐌽",
+       "rcfilters-limit-title": "ð\90\8d\84ð\90\8d\89ð\90\8c¾ð\90\8c° 𐌳𐌿 𐌱𐌰𐌽𐌳𐍅𐌾𐌰𐌽",
        "rcfilters-show-new-changes": "𐌱𐌰𐌽𐌳𐍅𐌴𐌹 𐌽𐌹𐌿𐌾𐌰𐍃𐍄𐍉𐍃 𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹𐌽𐌹𐌽𐍃",
        "rcfilters-filter-editsbyself-label": "𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹𐌽𐍉𐍃 𐍆𐍂𐌰𐌼 𐌸𐌿𐍃",
        "rcfilters-filter-editsbyother-label": "𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹𐌽𐍉𐍃 𐍆𐍂𐌰𐌼 𐌰𐌽𐌸𐌰𐍂𐌰𐌹𐌼",
        "uploadbtn": "𐌰𐍄𐌱𐌰𐌹𐍂 𐍆𐌰𐌾𐌻",
        "uploadlogpage": "𐌰𐍄𐌱𐌰𐌹𐍂 𐌻𐌰𐌿𐌲",
        "filedesc": "𐌼𐌰𐌿𐍂𐌲𐌿𐍃 𐍃𐌺𐌴𐌹𐍂𐌴𐌹𐌽𐍃",
-       "watchthisupload": "Witan so seido",
+       "watchthisupload": "𐌰𐍄𐍅𐌹𐍄 𐌸𐌰𐌼𐌼𐌰 𐍆𐌰𐌾𐌻𐌰",
        "imgfile": "𐍆𐌰𐌾𐌻",
        "listfiles": "𐍆𐌰𐌾𐌻𐌰𐍅𐌹𐌺𐍉",
        "file-anchor-link": "𐍆𐌰𐌾𐌻",
        "uctop": "(𐌷𐌰𐌿𐌱𐌹𐌸)",
        "month": "𐍆𐍂𐌰𐌼 𐌼𐌴𐌽𐍉𐌸 (𐌾𐌰𐌷 𐌰𐌹𐍂𐌹𐍃):",
        "year": "𐍆𐍂𐌰𐌼 𐌾𐌴𐍂𐌰 (𐌾𐌰𐌷 𐌰𐌹𐍂𐌹𐍃):",
-       "sp-contributions-newbies-sub": "ð\90\8c½ð\90\8c¹ð\90\8c¿ð\90\8c¾ð\90\8c°ð\90\8c¹ð\90\8c¼ ð\90\8cºð\90\8c°ð\90\8d\85ð\90\8d\84ð\90\8d\83ð\90\8c¾ð\90\8d\89𐌼",
+       "sp-contributions-newbies-sub": "ð\90\8c½ð\90\8c¹ð\90\8c¿ð\90\8c¾ð\90\8c°ð\90\8c¹ð\90\8c¼ ð\90\8d\82ð\90\8c°ð\90\8c·ð\90\8c½ð\90\8c´ð\90\8c¹ð\90\8c½ð\90\8c¹𐌼",
        "sp-contributions-blocklog": "𐍆𐌰𐌿𐍂𐌳𐌰𐌼𐌼𐌴𐌹𐌽𐌰𐌹𐍃 𐌲𐌰𐍆𐌰𐍃𐍄𐌰𐌹𐌽𐍃.",
        "sp-contributions-uploads": "𐌰𐍄𐌱𐌰𐌹𐍂𐌹𐌳𐍉𐍃 𐍅𐌰𐌹𐌷𐍄𐍃",
        "sp-contributions-logs": "𐌻𐌰𐌿𐌲𐌰",
        "tooltip-pt-mycontris": "A list of {{GENDER:|your}} 𐌱𐌹𐌰𐌿𐌺𐌰𐌹𐌽𐌴𐌹𐍃 𐌱𐍂𐌿𐌺𐌾𐌰𐌽𐌳𐌹𐍃",
        "tooltip-pt-login": "𐍄𐌹𐌼𐍂𐌾𐌰𐌶𐌰 𐌳𐌿 𐌰𐍄𐌲𐌰𐌲𐌲𐌰𐌽, 𐌹𐌸 𐌽𐌹𐍃𐍄 𐍃𐌺𐌿𐌻𐌳 𐌸𐌿𐍃",
        "tooltip-pt-logout": "𐌰𐍆𐌻𐌴𐌹𐌸",
-       "tooltip-pt-createaccount": "ð\90\8c±ð\90\8c°ð\90\8d\84ð\90\8c¹ð\90\8c¶ð\90\8d\89 ð\90\8c¹ð\90\8d\83ð\90\8d\84 ð\90\8c¸ð\90\8c¿ð\90\8d\83 ð\90\8d\83ð\90\8cºð\90\8c°ð\90\8d\80ð\90\8c¾ð\90\8c°ð\90\8c½ ð\90\8cºð\90\8c°ð\90\8d\85ð\90\8d\84ð\90\8d\83ð\90\8c¾ð\90\8d\89𐌽, 𐌹𐌸 𐍃𐌺𐌿𐌻𐌳 𐌽𐌹𐍃𐍄",
+       "tooltip-pt-createaccount": "ð\90\8c±ð\90\8c°ð\90\8d\84ð\90\8c¹ð\90\8c¶ð\90\8d\89 ð\90\8c¹ð\90\8d\83ð\90\8d\84 ð\90\8c¸ð\90\8c¿ð\90\8d\83 ð\90\8d\83ð\90\8cºð\90\8c°ð\90\8d\80ð\90\8c¾ð\90\8c°ð\90\8c½ ð\90\8d\82ð\90\8c°ð\90\8c·ð\90\8c½ð\90\8c´ð\90\8c¹𐌽, 𐌹𐌸 𐍃𐌺𐌿𐌻𐌳 𐌽𐌹𐍃𐍄",
        "tooltip-ca-talk": "𐌲𐌰𐍅𐌰𐌿𐍂𐌳𐌹 𐌱𐌹 𐌷𐌰𐌱𐌰𐌽𐌳𐌰𐌽 𐌻𐌰𐌿𐍆",
        "tooltip-ca-edit": "𐌹𐌽𐌼𐌰𐌹𐌳𐌴𐌹 𐌸𐌰𐌽𐌰 𐌻𐌰𐌿𐍆",
        "tooltip-ca-addsection": "𐌰𐌽𐌰𐍃𐍄𐍉𐌳𐌴𐌹 𐌽𐌹𐌿𐌾𐌰 𐌳𐌰𐌹𐌻",
        "tags-deactivate-submit": "𐌿𐌽𐌲𐌰𐌵𐌹𐌿𐌴𐌹",
        "logentry-delete-delete": "$1 {{GENDER:$2|𐌿𐍃𐌽𐌰𐌼}} 𐌻𐌰𐌿𐍆 $3",
        "logentry-move-move": "$1 {{GENDER:$2|𐌼𐌹𐌸𐍃𐌰𐍄𐌹𐌳𐌰}} 𐌻𐌰𐌿𐍆 $3 𐌳𐌿 $4",
-       "logentry-newusers-create": "ð\90\8c±ð\90\8d\82ð\90\8c¿ð\90\8cºð\90\8c¾ð\90\8c°ð\90\8cºð\90\8c°ð\90\8d\85ð\90\8d\84ð\90\8d\83ð\90\8c¾ð\90\8d\89 $1 𐍅𐌰𐍃 𐌲𐌰𐍃𐌺𐌰𐍀𐌰𐌽𐌰",
+       "logentry-newusers-create": "ð\90\8c±ð\90\8d\82ð\90\8c¿ð\90\8cºð\90\8c¾ð\90\8c°ð\90\8d\82ð\90\8c°ð\90\8c·ð\90\8c½ð\90\8c´ð\90\8c¹ð\90\8c½ð\90\8d\83 $1 𐍅𐌰𐍃 𐌲𐌰𐍃𐌺𐌰𐍀𐌰𐌽𐌰",
        "logentry-upload-upload": "$1 {{GENDER:$2|𐌿𐍆𐌰𐍂𐌱𐌰𐍂}} $3",
        "rightsnone": "(𐌽𐌹)",
        "searchsuggest-search": "𐍃𐍉𐌺𐌴𐌹",
index 7dc33b5..93ad441 100644 (file)
        "resetpass-submit-loggedin": "שינוי הסיסמה",
        "resetpass-submit-cancel": "ביטול",
        "resetpass-wrong-oldpass": "הסיסמה הזמנית או הנוכחית אינה תקינה.\nייתכן שכבר שינית את סיסמתך או שכבר ביקשת סיסמה זמנית חדשה.",
-       "resetpass-recycled": "×\99ש ×\9c×\90פס ×\90ת ×\94ס×\99ס×\9e×\94 ×\9cס×\99ס×\9e×\94 ×\94ש×\95× ×\94 ×\9eס×\99×\9eסתך הנוכחית.",
+       "resetpass-recycled": "×\99ש ×\9cשנ×\95ת ×\90ת ×\94ס×\99ס×\9e×\94 ×\9cס×\99ס×\9e×\94 ×\94ש×\95× ×\94 ×\9eס×\99ס×\9eתך הנוכחית.",
        "resetpass-temp-emailed": "נכנסת באמצעות סיסמה זמנית שנשלחה {{GENDER:|אליך|אלייך}} בדוא\"ל.\nכדי לסיים את הכניסה, יש להגדיר כאן סיסמה חדשה:",
        "resetpass-temp-password": "סיסמה זמנית:",
        "resetpass-abort-generic": "שינוי הסיסמה בוטל על־ידי הרחבה.",
        "resetpass-expired": "סיסמתך פקעה. נא להגדיר סיסמה חדשה כדי להיכנס.",
-       "resetpass-expired-soft": "×\94ס×\99ס×\9e×\94 ×©×\9c×\9a ×¤×§×¢×\94, ×\95צר×\99×\9a ×\9c×\90פס ×\90×\95ת×\94. ×\99ש ×\9c×\91×\97×\95ר ×¡×\99ס×\9e×\94 ×\97×\93ש×\94 ×\9bעת, ×\90×\95 ×\9c×\9c×\97×\95×¥ ×¢×\9c \"{{int:authprovider-resetpass-skip-label}}\" ×\9b×\93×\99 ×\9c×\90פס אותה מאוחר יותר.",
-       "resetpass-validity-soft": "×\94ס×\99ס×\9e×\94 ×©×\9c×\9a ×\90×\99× ×\94 ×ª×§×\99× ×\94: $1\n\n×\99ש ×\9c×\91×\97×\95ר ×¡×\99ס×\9e×\94 ×\97×\93ש×\94 ×\9bעת ×\90×\95 ×\9c×\9c×\97×\95×¥ ×¢×\9c \"{{int:authprovider-resetpass-skip-label}}\" ×\9b×\93×\99 ×\9c×\90פס את הסיסמה מאוחר יותר.",
+       "resetpass-expired-soft": "×\94ס×\99ס×\9e×\94 ×©×\9c×\9a ×¤×§×¢×\94, ×\95צר×\99×\9a ×\9cשנ×\95ת ×\90×\95ת×\94. ×\99ש ×\9c×\91×\97×\95ר ×¡×\99ס×\9e×\94 ×\97×\93ש×\94 ×\9bעת, ×\90×\95 ×\9c×\9c×\97×\95×¥ ×¢×\9c \"{{int:authprovider-resetpass-skip-label}}\" ×\9b×\93×\99 ×\9cשנ×\95ת אותה מאוחר יותר.",
+       "resetpass-validity-soft": "×\94ס×\99ס×\9e×\94 ×©×\9c×\9a ×\90×\99× ×\94 ×ª×§×\99× ×\94: $1\n\n×\99ש ×\9c×\91×\97×\95ר ×¡×\99ס×\9e×\94 ×\97×\93ש×\94 ×\9bעת ×\90×\95 ×\9c×\9c×\97×\95×¥ ×¢×\9c \"{{int:authprovider-resetpass-skip-label}}\" ×\9b×\93×\99 ×\9cשנ×\95ת את הסיסמה מאוחר יותר.",
        "passwordreset": "איפוס סיסמה",
        "passwordreset-text-one": "יש למלא טופס זה כדי לקבל סיסמה זמנית בדוא\"ל.",
        "passwordreset-text-many": "{{PLURAL:$1||יש למלא אחד מהשדות הבאים כדי לקבל סיסמה זמנית בדוא\"ל.}}",
        "rcfilters-other-review-tools": "כלי סקירה אחרים",
        "rcfilters-group-results-by-page": "חלוקה התוצאות לקבוצות לפי דף",
        "rcfilters-activefilters": "מסננים פעילים",
+       "rcfilters-activefilters-hide": "הסתרה",
+       "rcfilters-activefilters-show": "הצגה",
        "rcfilters-advancedfilters": "מסננים מתקדמים",
        "rcfilters-limit-title": "כמה תוצאות להראות",
        "rcfilters-limit-and-date-label": "{{PLURAL:$1|שינוי אחד|$1 שינויים}}, $2",
        "rcfilters-savedqueries-rename": "שינוי שם",
        "rcfilters-savedqueries-setdefault": "הגדרה כברירת מחדל",
        "rcfilters-savedqueries-unsetdefault": "ביטול הגדרה כברירת מחדל",
-       "rcfilters-savedqueries-remove": "×\94סרה",
+       "rcfilters-savedqueries-remove": "×\9e×\97×\99קה",
        "rcfilters-savedqueries-new-name-label": "שם",
        "rcfilters-savedqueries-new-name-placeholder": "תיאור מטרת המסנן",
        "rcfilters-savedqueries-apply-label": "יצירת המסנן",
        "rcfilters-empty-filter": "אין מסננים פעילים. כל התרומות מוצגות.",
        "rcfilters-filterlist-title": "מסננים",
        "rcfilters-filterlist-whatsthis": "איך הם עובדים?",
-       "rcfilters-filterlist-feedbacklink": "באפשרותך לספר לנו מה דעתך על כלי הסינון (החדשים) האלה",
+       "rcfilters-filterlist-feedbacklink": "באפשרותך לספר לנו מה דעתך על כלי הסינון האלה",
        "rcfilters-highlightbutton-title": "הבלטת התוצאות",
        "rcfilters-highlightmenu-title": "בחירת צבע",
        "rcfilters-highlightmenu-help": "בחירת צבע להבלטת מאפיין זה",
        "tooltip-pt-preferences": "ההעדפות שלך",
        "tooltip-pt-watchlist": "רשימת הדפים {{GENDER:|שאתה עוקב|שאת עוקבת}} אחרי השינויים בהם",
        "tooltip-pt-mycontris": "רשימת העריכות שביצעת",
-       "tooltip-pt-anoncontribs": "רשימת העריכות שנעשו מכתובת ה־IP הזאת",
+       "tooltip-pt-anoncontribs": "רשימת העריכות שנעשו מכתובת IP זו",
        "tooltip-pt-login": "מומלץ להיכנס לחשבון, אך אין חובה לעשות זאת",
        "tooltip-pt-login-private": "יש להיכנס לחשבון כדי להשתמש באתר הוויקי הזה",
        "tooltip-pt-logout": "יציאה מהחשבון",
        "tooltip-summary": "תיאור קצר של השינויים שביצעת",
        "common.css": "/* הסגנונות הנכתבים כאן ישפיעו על כל העיצובים */",
        "print.css": "/* הסגנונות הנכתבים כאן ישפיעו על הפלט בהדפסה בלבד */",
-       "noscript.css": "/* הסגנונות הנכתבים כאן ישפיעו על משתמשים עם ג'אווה-סקריפט מבוטל */",
+       "noscript.css": "/* הסגנונות הנכתבים כאן ישפיעו על משתמשים עם ג'אווה־סקריפט מבוטל */",
        "group-autoconfirmed.css": "/* הסגנונות הנכתבים כאן ישפיעו על משתמשים ותיקים בלבד */",
        "group-user.css": "/* הסגנונות הנכתבים כאן ישפיעו על משתמשים רשומים בלבד */",
        "group-bot.css": "/* הסגנונות הנכתבים כאן ישפיעו על בוטים בלבד */",
        "group-sysop.css": "/* הסגנונות הנכתבים כאן ישפיעו על מפעילי מערכת בלבד */",
        "group-bureaucrat.css": "/* הסגנונות הנכתבים כאן ישפיעו על ביורוקרטים בלבד */",
        "common.json": "/* כל ה־JSON שנכתב כאן ייטען עבור כל המשתמשים בכל טעינת עמוד. */",
-       "common.js": "/* ×\9b×\9c ×\94סקר×\99פ×\98×\99×\9d ×©נכתבים כאן ייטענו עבור כל המשתמשים בכל טעינת עמוד. */",
+       "common.js": "/* ×\9b×\9c ×\94סקר×\99פ×\98×\99×\9d ×\94נכתבים כאן ייטענו עבור כל המשתמשים בכל טעינת עמוד. */",
        "group-autoconfirmed.js": "/* כל הסקריפטים הנכתבים כאן ייטענו עבור משתמשים ותיקים בלבד */",
        "group-user.js": "/* כל הסקריפטים הנכתבים כאן ייטענו עבור משתמשים רשומים בלבד */",
        "group-bot.js": "/* כל הסקריפטים הנכתבים כאן ייטענו עבור בוטים בלבד */",
        "spam_reverting": "שחזור לגרסה אחרונה שלא כוללת קישורים ל־$1",
        "spam_blanking": "כל הגרסאות כוללות קישורים ל־$1, מרוקן את הדף",
        "spam_deleting": "כל הגרסאות כוללות קישורים ל־$1, מוחק את הדף",
-       "simpleantispam-label": "×\91×\93×\99קת ×\90× ×\98×\99־ספ×\90×\9d.\n<strong>×\90×\9c</strong> ×ª×\9e×\9c×\90×\95 שדה זה!",
+       "simpleantispam-label": "×\91×\93×\99קת ×\90× ×\98×\99־ספ×\90×\9d.\n<strong>×\90×\99×\9f</strong> ×\9c×\9e×\9c×\90 שדה זה!",
        "pageinfo-title": "מידע על הדף \"$1\"",
        "pageinfo-not-current": "מצטערים, לא ניתן להציג את המידע הזה לגרסאות ישנות.",
        "pageinfo-header-basic": "מידע בסיסי",
        "pageinfo-magic-words": "{{PLURAL:$1|מילת קסם|מילות קסם}} ($1)",
        "pageinfo-hidden-categories": "{{PLURAL:$1|קטגוריה מוסתרת|קטגוריות מוסתרות}} ($1)",
        "pageinfo-templates": "{{PLURAL:$1|תבנית מוכללת|תבניות מוכללות}} ($1)",
-       "pageinfo-transclusions": "{{PLURAL:$1|דף|דפים}} שמוכלל בהם ($1)",
+       "pageinfo-transclusions": "{{PLURAL:$1|דף|דפים}} שמוכלל {{PLURAL:$1|בו|בהם}} ($1)",
        "pageinfo-toolboxlink": "מידע על הדף",
        "pageinfo-redirectsto": "מפנה אל",
        "pageinfo-redirectsto-info": "מידע",
        "markedaspatrolled": "השינוי סומן כבדוק",
        "markedaspatrolledtext": "הגרסה שבחרת בדף [[:$1]] סומנה כבדוקה.",
        "rcpatroldisabled": "אפשרות סימון השינויים כבדוקים מבוטלת",
-       "rcpatroldisabledtext": "ת×\9b×\95נת ×¡×\99×\9e×\95×\9f ×©×\99× ×\95×\99×\99×\9d ×\9b×\91×\93×\95ק×\99×\9d ×\91×\93×£ ×\94ש×\99× ×\95×\99×\99×\9d ×\94×\90×\97ר×\95× ×\99×\9d ×\91×\95×\98×\9c×\94.",
+       "rcpatroldisabledtext": "ת×\9b×\95נת ×¡×\99×\9e×\95×\9f ×\94ש×\99× ×\95×\99×\99×\9d ×\9b×\91×\93×\95ק×\99×\9d ×\91×\93×£ ×\94ש×\99× ×\95×\99×\99×\9d ×\94×\90×\97ר×\95× ×\99×\9d ×\9e×\91×\95×\98×\9cת ×\9bר×\92×¢.",
        "markedaspatrollederror": "לא ניתן לסמן כבדוק",
        "markedaspatrollederrortext": "יש לציין גרסה שברצונך לסמן כבדוקה.",
-       "markedaspatrollederror-noautopatrol": "×\90×\99× ×\9a ×\9e×\95רש×\94 לסמן שינויים של עצמך כבדוקים.",
+       "markedaspatrollederror-noautopatrol": "×\90×\99×\9f ×\91×\90פשר×\95ת×\9a לסמן שינויים של עצמך כבדוקים.",
        "markedaspatrollednotify": "שינוי זה בדף \"$1\" סומן כבדוק.",
        "markedaspatrollederrornotify": "סימון השינוי כבדוק נכשל.",
        "patrol-log-page": "יומן שינויים בדוקים",
        "file-info-png-looped": "בלולאה",
        "file-info-png-repeat": "מוצג {{PLURAL:$1|פעם אחת|פעמיים|$1 פעמים}}",
        "file-info-png-frames": "{{PLURAL:$1|תמונה אחת|$1 תמונות}}",
-       "file-no-thumb-animation": "'''לתשומת לבך: בשל מגבלות טכניות, תמונות ממוזערות של הקובץ הזה לא תהיינה מונפשות.'''",
-       "file-no-thumb-animation-gif": "'''לתשומת לבך: בשל מגבלות טכניות, תמונות ממוזערות של תמונות GIF בעלות רזולוציה גבוהה כמו זאת לא תהיינה מונפשות.'''",
+       "file-no-thumb-animation": "<strong>לתשומת לבך: בשל מגבלות טכניות, תמונות ממוזערות של הקובץ הזה לא תהיינה מונפשות.</strong>",
+       "file-no-thumb-animation-gif": "<strong>לתשומת לבך: בשל מגבלות טכניות, תמונות ממוזערות של תמונות GIF בעלות רזולוציה גבוהה כמו זאת לא תהיינה מונפשות.</strong>",
        "newimages": "גלריית קבצים חדשים",
        "imagelisttext": "להלן רשימה של {{PLURAL:$1|קובץ אחד|$1 קבצים}}, ממוינים $2:",
        "newimages-summary": "דף מיוחד זה מציג את הקבצים האחרונים שהועלו.",
-       "newimages-legend": "×\9eסנן",
+       "newimages-legend": "ס×\99× ×\95ן",
        "newimages-label": "שם הקובץ (או חלק ממנו):",
        "newimages-user": "כתובת IP או שם משתמש",
        "newimages-newbies": "הצגת תרומות של משתמשים חדשים בלבד",
        "bydate": "לפי תאריך",
        "sp-newimages-showfrom": "הצגת קבצים חדשים החל מ־$2, $1",
        "seconds-abbrev": "{{PLURAL:$1|שנייה|$1 שניות}}",
-       "minutes-abbrev": "{{PLURAL:$1|דקה|$1 דק'}}",
+       "minutes-abbrev": "{{PLURAL:$1|דקה|$1 דקות}}",
        "hours-abbrev": "{{PLURAL:$1|שעה|שעתיים|$1 שעות}}",
        "days-abbrev": "{{PLURAL:$1|יום|יומיים|$1 ימים}}",
        "seconds": "{{PLURAL:$1|שנייה|$1 שניות}}",
index d88d794..966953c 100644 (file)
        "lag-warn-normal": "Promjene načinjene prije manje od $1 {{PLURAL:$1|sekunde|sekundi}} možda ne će biti prikazane na ovom popisu.",
        "lag-warn-high": "Zbog preopterećenosti poslužitelja na kom je baza podataka, izmjene novije od $1 {{PLURAL:$1|sekunde|sekundi}} možda ne će biti prikazane na ovom popisu.",
        "watchlistedit-normal-title": "Uredi popis praćenja",
-       "watchlistedit-normal-legend": "Ukloni stranice iz popisa praćenja",
+       "watchlistedit-normal-legend": "Uklanjanje naslova s popisa praćenja",
        "watchlistedit-normal-explain": "Prikazane su stranice na Vašem popisu praćenja.\nDa uklonite stranicu s popisa praćenja, označite kućicu kraj nje i kliknite gumb \"{{int:Watchlistedit-normal-submit}}\".\nMožete također [[Special:EditWatchlist/raw|uređivati ovaj popis u okviru za uređivanje]].",
        "watchlistedit-normal-submit": "Ukloni stranice",
        "watchlistedit-normal-done": "{{PLURAL:$1|1 stranica je uklonjena|$1 stranice su uklonjene|$1 stranica je uklonjeno}} iz Vašeg popisa praćenja:",
index 97b169e..6bd9201 100644 (file)
        "resetpass-temp-password": "Ideiglenes jelszó:",
        "resetpass-abort-generic": "A jelszómódosítást megszakította egy kiterjesztés.",
        "resetpass-expired": "A jelszavad lejárt. Adjál meg egy új jelszót a bejelentkezéshez!",
-       "resetpass-expired-soft": "A jelszavad lejárt, ezért újat kell beállítanod. Válassz most egy új jelszót, vagy kattints a {{int:authprovider-resetpass-skip-label}} gombra, ha később akarod csak beállítani.",
-       "resetpass-validity-soft": "A jelszavad érvénytelen: $1\n\nAdj meg egy új jelszót most, vagy kattints a „{{int:authprovider-resetpass-skip-label}}” gombra, ha később akarod megadni.",
+       "resetpass-expired-soft": "A jelszavad lejárt, ezért újat kell beállítanod. Válassz most egy új jelszót, vagy kattints a {{int:authprovider-resetpass-skip-label}} gombra, ha csak később akarod megváltoztatni.",
+       "resetpass-validity-soft": "A jelszavad érvénytelen: $1\n\nAdj meg egy új jelszót most, vagy kattints a „{{int:authprovider-resetpass-skip-label}}” gombra, ha csak később akarod megváltoztatni.",
        "passwordreset": "Jelszó visszaállítása",
        "passwordreset-text-one": "A jelszó átmeneti beállításához töltsd ki az űrlapot.",
        "passwordreset-text-many": "{{PLURAL:$1|Az átmeneti jelszó elküldéséhez töltsd ki az alábbi mezők egyikét.}}",
        "rcfilters-other-review-tools": "Egyéb hasznos hivatkozások",
        "rcfilters-group-results-by-page": "Eredmények csoportosítása lapok szerint",
        "rcfilters-activefilters": "Aktív szűrők",
+       "rcfilters-activefilters-hide": "Elrejt",
+       "rcfilters-activefilters-show": "Mutat",
        "rcfilters-advancedfilters": "Haladó szűrők",
        "rcfilters-limit-title": "Megjelenítendő találatok száma",
        "rcfilters-limit-and-date-label": "$1 változtatás, $2",
        "rcfilters-savedqueries-rename": "Átnevezés",
        "rcfilters-savedqueries-setdefault": "Beállítás alapértelmezettként",
        "rcfilters-savedqueries-unsetdefault": "Eltávolítás, mint alapértelmezés",
-       "rcfilters-savedqueries-remove": "Eltávolítás",
+       "rcfilters-savedqueries-remove": "Törlés",
        "rcfilters-savedqueries-new-name-label": "Név",
        "rcfilters-savedqueries-new-name-placeholder": "Írd le a szűrő célját.",
        "rcfilters-savedqueries-apply-label": "Gyors hivatkozás létrehozása",
        "rcfilters-empty-filter": "Nincs aktív szűrő. Minden közreműködés látható.",
        "rcfilters-filterlist-title": "Szűrők",
        "rcfilters-filterlist-whatsthis": "Hogy működnek?",
-       "rcfilters-filterlist-feedbacklink": "Mond el nekünk, mit gondolsz az (új) szűrőkről",
+       "rcfilters-filterlist-feedbacklink": "Mond el nekünk, mit gondolsz ezekről a szűrőkről",
        "rcfilters-highlightbutton-title": "Kiemelt találatok",
        "rcfilters-highlightmenu-title": "Válassz egy színt",
        "rcfilters-highlightmenu-help": "Válassz színt ennek a tulajdonságnak kijelöléséhez",
index bf3d2df..cc3e900 100644 (file)
        "protectedpagetext": "Ер оагIув лораяь я цу тIа хувцамаш дергдоацаш.",
        "viewsourcetext": "Укх оагIон чухьнахьарча текстах бIаргатоха а, цунах кеп яьккха а, йиш я хьа.",
        "virus-unknownscanner": "йовзанза антивирус:",
+       "logouttext": "<strong>Оаша болх чакхбаьккхаб.</strong>\n\nНекоторые страницы могут продолжить отображаться так, словно вы все ещё не завершили сеанс. Для борьбы с этим явлением обновите кэш браузера.",
        "welcomeuser": "Марша воагIалва, доакъашхо $1!",
        "yourname": "Дагара йоазон цIи:",
        "userlogin-yourname": "Доакъашхочун цӀи",
        "preview": "Хьалххе бIаргтохар",
        "showpreview": "Хьалххе бIаргтохар",
        "showdiff": "Даь дола хувцамаш",
-       "anoneditwarning": "<strong>Теркам бе!</strong> Хьо автор хинна система чуваьннавац. Нагахьа санна Iа моллагIа хувцам бой, Хьа IP-адрес дийла массанен бIаргагуш хургда. Нагахьа санна Хьо <strong>[$1 хьачувоале]</strong> е <strong>[$2 дагара йоазув хьакхолле]</strong>, нийсдараш (хувцамаш) бувзам болаш хургда Хьа доакъашхой цIерца, иштта кхыдола толажагIи гIойленагIи дола дикаьш хургда Хьона.",
+       "anoneditwarning": "<strong>Теркам бе!</strong> Хьо ражача дIаязвеннавац. Нагахьа санна Iа моллагIа хувцам бой, Хьа IP-цIай хьадийла массанена бIаргагуш хургда. Нагахьа санна Хьо <strong>[$1 ражача хьачувоале]</strong> е Iа <strong>[$2 дагара йоазув хьакхолле]</strong>, хьа тоадар (хувцамаш) бувзам болаш хургда Хьа цIерца, иштта кхыдола толашагIеи гIойленагIеи дола дикаьш хургда Хьона.",
        "summary-preview": "Хувцама сурт оттадарах хьалхе бӀаргтохар:",
        "subject-preview": "Темах/кепакертах хьалххе бӀаргтохар:",
        "blockedtitle": "Доакъашхочун чIега техаб",
        "enotif_reset": "Белгалъе еррига оагӀонаш бӀаргтехача санна",
        "enotif_impersonal_salutation": "{{grammar:genitive|{{SITENAME}}}} – доакъашхо",
        "deletepage": "ДIаяккха оагIув",
+       "excontent": "чудар: «$1»",
+       "excontentauthor": "чухьнахьадар: «$1», цу оагIон цаI марка воаца автор ва [[Special:Contributions/$2|$2]] ([[User talk:$2|дувцара оагIув]])",
        "delete-confirm": "$1 — дӀаяккхар",
        "delete-legend": "ДӀаяккхар",
        "confirmdeletetext": "Оаш дIадийхад бIарчча дIадаккхар оагIон а (е сурта), цун деррига хувцара истори а. '''Дехар да''', бакъде шоай из бокъонца де безам болаш долга а, из дича хургдар кхеташ долга а, из дар укх [[{{MediaWiki:Policy-url}}|бокъонашца]] долга.",
        "restriction-move": "ЦIи хувцаp",
        "restriction-create": "Хьакхоллар",
        "restriction-upload": "Доттар",
+       "undeletepage": "ДӀаяьхача оагӀонашка хьажар а уж меттаоттаяр а",
        "undeletelink": "бIаргтоха/юхадаккха",
        "undeleteviewlink": "хьажа",
+       "undeletedpage": "'''Меттаоттаяьй оагӀув «$1».'''\n\nТIехьарча хана дӀаяьха а юхаметтаоттаяь а хиннача оагIонашка хьажара духьа хьадела [[Special:Log/delete|тептар]].",
        "undelete-search-submit": "Хьалáха",
        "namespace": "ЦIерий моттигаш:",
        "invert": "Хержар юхадаккха",
        "movelogpage": "ЦӀераш хувцара тептар",
        "movereason": "Бахьан:",
        "revertmove": "юха",
+       "delete_and_move_reason": "ДӀаяьккхай укх оагIон «[[$1]]» цӀи хувцара духьа",
        "export": "Оагӏоний экспорт",
        "allmessagesname": "Хоам",
        "allmessagesdefault": "Массаза йола текст",
index 461be91..f1e5433 100644 (file)
        "monthsall": "omna",
        "confirmemail": "Konfirmez adreso di e-posto",
        "confirmemail_needlogin": "Vu mustas $1 pro konfirmar vua adreso di e-posto.",
+       "confirmemail_loggedin": "Vua adreso di e-posto konfirmesis nun.",
        "notificationemail_subject_changed": "L'adreso di e-posto en {{SITENAME}} modifikesis",
        "scarytranscludetoolong": "[URL es tro longa]",
        "deletedwhileediting": "'''Averto''': Ta pagino efacesis pos ke vu redakteskis!",
index 6cb93ab..c124df0 100644 (file)
                        "Manvydasz",
                        "S4b1nuz E.656",
                        "Daimona Eaytoy",
-                       "Sarah Bernabei"
+                       "Sarah Bernabei",
+                       "Wedhro"
                ]
        },
        "tog-underline": "Sottolinea i collegamenti:",
        "resetpass-submit-loggedin": "Cambia password",
        "resetpass-submit-cancel": "Annulla",
        "resetpass-wrong-oldpass": "Password temporanea o attuale non valida.\nLa password potrebbe essere stata già modificata, oppure potrebbe essere stata richiesta una nuova password temporanea.",
-       "resetpass-recycled": "Si prega di reimpostare con una password diversa dalla password attuale.",
+       "resetpass-recycled": "Si prega di modificare la tua password con una diversa da quella attuale.",
        "resetpass-temp-emailed": "L'accesso è stato effettuato con un codice temporaneo, inviato via email. Per completare la registrazione, è necessario impostare una nuova password qui:",
        "resetpass-temp-password": "Password temporanea:",
        "resetpass-abort-generic": "La modifica della password è stata interrotta da un'estensione.",
        "resetpass-expired": "La password è scaduta. Si prega di impostare una nuova password per effettuare l'accesso.",
-       "resetpass-expired-soft": "La tua password è scaduta e deve essere reimpostata. Si prega di scegliere una nuova password o fare clic su \"{{int:authprovider-resetpass-skip-label}}\" per reimpostarla successivamente.",
-       "resetpass-validity-soft": "La tua password non è valida: $1\n\nSi prega di scegliere una nuova password o fare clic su \"{{int:authprovider-resetpass-skip-label}}\" per reimpostarla successivamente.",
+       "resetpass-expired-soft": "La tua password è scaduta e deve essere modificata. Si prega di scegliere una nuova password o fare clic su \"{{int:authprovider-resetpass-skip-label}}\" per modificarla successivamente.",
+       "resetpass-validity-soft": "La tua password non è valida: $1\n\nSi prega di scegliere una nuova password o fare clic su \"{{int:authprovider-resetpass-skip-label}}\" per modificarla successivamente.",
        "passwordreset": "Reimposta password",
        "passwordreset-text-one": "Compila questo modulo per reimpostare la tua password.",
        "passwordreset-text-many": "{{PLURAL:$1|Compila uno dei campi per ricevere una password temporanea tramite email.}}",
        "default": "predefinito",
        "prefs-files": "File",
        "prefs-custom-css": "CSS personalizzato",
+       "prefs-custom-json": "JSON personalizzato",
        "prefs-custom-js": "JavaScript personalizzato",
        "prefs-common-config": "CSS/JSON/JavaScript condiviso per tutti i temi:",
        "prefs-reset-intro": "È possibile usare questa pagina per reimpostare le proprie preferenze a quelle predefinite del sito.\nL'operazione non può essere annullata.",
        "right-editcontentmodel": "Modifica il modello di contenuto di una pagina",
        "right-editinterface": "Modifica l'interfaccia utente",
        "right-editusercss": "Modifica i file CSS di altri utenti",
+       "right-edituserjson": "Modifica i file JSON di altri utenti",
        "right-edituserjs": "Modifica i file JS di altri utenti",
        "right-editmyusercss": "Modifica il file CSS del proprio utente",
        "right-editmyuserjson": "Modifica il file JSON del proprio utente",
        "rcfilters-other-review-tools": "Altri strumenti di revisione",
        "rcfilters-group-results-by-page": "Raggruppa risultati per pagina",
        "rcfilters-activefilters": "Filtri attivi",
+       "rcfilters-activefilters-hide": "Nascondi",
+       "rcfilters-activefilters-show": "Mostra",
        "rcfilters-advancedfilters": "Filtri avanzati",
        "rcfilters-limit-title": "Risultati da mostrare",
        "rcfilters-limit-and-date-label": "$1 {{PLURAL:$1|modifica|modifiche}}, $2",
        "rcfilters-savedqueries-rename": "Rinomina",
        "rcfilters-savedqueries-setdefault": "Imposta come predefinito",
        "rcfilters-savedqueries-unsetdefault": "Rimuovi come predefinito",
-       "rcfilters-savedqueries-remove": "Rimuovi",
+       "rcfilters-savedqueries-remove": "Cancella",
        "rcfilters-savedqueries-new-name-label": "Nome",
        "rcfilters-savedqueries-new-name-placeholder": "Descrivi lo scopo del filtro",
        "rcfilters-savedqueries-apply-label": "Crea filtro",
        "rcfilters-empty-filter": "Nessun filtro attivo. Sono mostrati tutti i contributi.",
        "rcfilters-filterlist-title": "Filtri",
        "rcfilters-filterlist-whatsthis": "Come funzionano?",
-       "rcfilters-filterlist-feedbacklink": "Dicci cosa ne pensi su questi (nuovi) strumenti di filtraggio",
+       "rcfilters-filterlist-feedbacklink": "Dicci cosa ne pensi su questi strumenti di filtraggio",
        "rcfilters-highlightbutton-title": "Evidenzia risultati",
        "rcfilters-highlightmenu-title": "Seleziona un colore",
        "rcfilters-highlightmenu-help": "Seleziona un colore per evidenziare questa proprietà",
        "uploadstash-bad-path": "Il percorso non esiste.",
        "uploadstash-bad-path-invalid": "Il percorso non è valido.",
        "uploadstash-bad-path-unknown-type": "Tipo sconosciuto \"$1\".",
+       "uploadstash-file-not-found-no-thumb": "Non è stato possibile ottenere la miniatura.",
+       "uploadstash-file-not-found-no-remote-thumb": "Recupero della miniatura fallito: $1\nURL = $2",
        "uploadstash-zero-length": "Il file ha lunghezza zero.",
        "invalid-chunk-offset": "Offset della parte non valido.",
        "img-auth-accessdenied": "Accesso negato",
        "uncategorizedcategories": "Categorie prive di categorie",
        "uncategorizedimages": "File privi di categorie",
        "uncategorizedtemplates": "Template privi di categorie",
+       "uncategorized-categories-exceptionlist": "# Contiene un elenco di categorie che non dovrebbero figurare in Special:UncategorizedCategories. Una categoria per linea, a cominciare da \"*\". Le linee che iniziano per un altro carattere (spazi bianchi inclusi) saranno ignorate. Usa «#» per aggiungere commenti.",
        "unusedcategories": "Categorie vuote",
        "unusedimages": "File non utilizzati",
        "wantedcategories": "Categorie richieste",
        "ipb_blocked_as_range": "Errore: L'indirizzo IP $1 non è soggetto a blocco individuale e non può essere sbloccato. Il blocco è invece attivo a livello dell'intervallo $2, che può essere sbloccato.",
        "ip_range_invalid": "Intervallo di indirizzi IP non valido.",
        "ip_range_toolarge": "Non è possibile bloccare range superiori al /$1",
+       "ip_range_exceeded": "L'intervallo di IP supera il limite massimo. Intervallo contentito: /$1.",
+       "ip_range_toolow": "Gli intervalli di IP non sono consentiti, di fatto.",
        "proxyblocker": "Blocco dei proxy aperti",
        "proxyblockreason": "Questo indirizzo IP è stato bloccato perché risulta essere un proxy aperto. Si prega di contattare il proprio fornitore di accesso a Internet o il supporto tecnico e informarli di questo grave problema di sicurezza.",
        "sorbsreason": "Questo indirizzo IP è elencato come proxy aperto nella lista DNSBL utilizzata da {{SITENAME}}.",
        "unlinkaccounts-success": "L'utenza è stata scollegata.",
        "authenticationdatachange-ignored": "Il cambiamento dei dati di autenticazione non è stato gestito. Forse non è stato configurato nessun provider?",
        "userjsispublic": "Ricorda: le sottopagine JavaScript non devono contenere dati riservati poichè sono visualizzabili da altri utenti.",
+       "userjsonispublic": "Ricorda: le sottopagine JSON non dovrebbero contenere dati personali perché sono visibili agli altri utenti.",
        "usercssispublic": "Ricorda: le sottopagine CSS non devono contenere dati riservati poichè sono visualizzabili da altri utenti.",
        "restrictionsfield-badip": "Indirizzo IP o intervallo non valido: $1",
        "restrictionsfield-label": "Intervalli IP consentiti:",
index dfb44c5..de57a0d 100644 (file)
        "resetpass-submit-loggedin": "비밀번호 바꾸기",
        "resetpass-submit-cancel": "취소",
        "resetpass-wrong-oldpass": "비밀번호가 잘못되었거나 현재의 비밀번호와 같습니다.\n이미 비밀번호를 바꾸었거나 새 임시 비밀번호를 요청했을 수 있습니다.",
-       "resetpass-recycled": "현재 비밀번호와 다른 비밀번호로 재설정해주세요.",
+       "resetpass-recycled": "현재 비밀번호와 다른 비밀번호로 변경해주세요.",
        "resetpass-temp-emailed": "임시 이메일 코드로 로그인되어 있습니다.\n로그인을 마치려면, 여기서 새 비밀번호를 설정해야 합니다:",
        "resetpass-temp-password": "임시 비밀번호:",
        "resetpass-abort-generic": "비밀번호 바꾸기가 확장 기능에 의해 중단되었습니다.",
        "resetpass-expired": "비밀번호가 만료되었습니다. 로그인하려면 새 비밀번호를 설정해야 합니다.",
-       "resetpass-expired-soft": "비밀번호가 만료되어 재설정해야 합니다. 지금 새로운 비밀번호를 선택하거나, \"{{int:authprovider-resetpass-skip-label}}\"를 클릭하고 나중에 재설정해주세요.",
-       "resetpass-validity-soft": "귀하의 비밀번호는 유효하지 않습니다: $1\n\n새로운 비밀번호를 지금 설정하거나, \"{{int:authprovider-resetpass-skip-label}}\"을 눌러 나중에 재설정하세요.",
+       "resetpass-expired-soft": "비밀번호가 만료되어 변경해야 합니다. 지금 새로운 비밀번호를 선택하거나, \"{{int:authprovider-resetpass-skip-label}}\"를 클릭하고 나중에 재설정해주세요.",
+       "resetpass-validity-soft": "귀하의 비밀번호는 유효하지 않습니다: $1\n\n새로운 비밀번호를 지금 설정하거나, \"{{int:authprovider-resetpass-skip-label}}\"을 눌러 나중에 변경하세요.",
        "passwordreset": "비밀번호 재설정",
        "passwordreset-text-one": "이메일을 통해 임시 비밀번호를 받으려면 이 양식을 채우세요.",
        "passwordreset-text-many": "{{PLURAL:$1|이메일을 통해 임시 비밀번호를 받으려면 필드 중 하나를 채우세요.}}",
        "recentchanges": "최근 바뀜",
        "recentchanges-legend": "최근 바뀜 설정",
        "recentchanges-summary": "이 페이지에서 위키의 최근 바뀜을 추적합니다.",
-       "recentchanges-noresult": "ì§\80ì \95í\95\9c ì¡°ê±´ê³¼ ì\9d¼ì¹\98í\95\98ë\8a\94 ì£¼ì\96´ì§\84 ê¸°ê°\84 ë\8f\99ì\95\88 바뀜이 없습니다.",
+       "recentchanges-noresult": "주ì\96´ì§\84 ê¸°ê°\84 ë\8f\99ì\95\88ì\97\90 ì\9d´ ì¡°ê±´ë\93¤ì\97\90 ë§\9eë\8a\94 바뀜이 없습니다.",
        "recentchanges-timeout": "이 검색의 시간이 초과되었습니다. 다른 검색 변수를 사용할 수 있습니다.",
        "recentchanges-network": "기술적인 문제로 결과를 불러올 수 없습니다. 페이지를 다시 새로 고침해 주십시오.",
        "recentchanges-notargetpage": "해당 문서에 관한 변경사항을 보려면 상단에 문서 제목을 입력하십시오.",
        "rcfilters-other-review-tools": "다른 검토 도구",
        "rcfilters-group-results-by-page": "문서별로 묶음",
        "rcfilters-activefilters": "사용 중인 필터",
+       "rcfilters-activefilters-hide": "숨기기",
+       "rcfilters-activefilters-show": "보이기",
        "rcfilters-advancedfilters": "고급 필터",
        "rcfilters-limit-title": "표시할 결과 수",
        "rcfilters-limit-and-date-label": "{{PLURAL:$1|변경사항}} $1개, $2",
        "rcfilters-savedqueries-rename": "이름 바꾸기",
        "rcfilters-savedqueries-setdefault": "기본값으로 설정",
        "rcfilters-savedqueries-unsetdefault": "기본값으로 제거",
-       "rcfilters-savedqueries-remove": "ì \9cê±°",
+       "rcfilters-savedqueries-remove": "ì\82­ì \9c",
        "rcfilters-savedqueries-new-name-label": "이름",
        "rcfilters-savedqueries-new-name-placeholder": "필터의 목적을 설명하세요",
        "rcfilters-savedqueries-apply-label": "필터 만들기",
        "rcfilters-empty-filter": "활성화된 필터가 없습니다. 모든 기여가 표시됩니다.",
        "rcfilters-filterlist-title": "필터",
        "rcfilters-filterlist-whatsthis": "이것들이 어떻게 동작합니까?",
-       "rcfilters-filterlist-feedbacklink": "이 (새로운) 필터 도구에 대한 의견을 주세요",
+       "rcfilters-filterlist-feedbacklink": "이 필터 도구에 대한 의견을 주세요",
        "rcfilters-highlightbutton-title": "결과 강조",
        "rcfilters-highlightmenu-title": "색 선택",
        "rcfilters-highlightmenu-help": "이 속성을 강조할 색을 선택하십시오",
index 1438232..d194391 100644 (file)
@@ -21,7 +21,8 @@
                        "Matma Rex",
                        "Stryn",
                        "Cûndûllah el-Kurdî",
-                       "Fitoschido"
+                       "Fitoschido",
+                       "Kur"
                ]
        },
        "tog-underline": "Xetekê di bin girêdanê de çêke:",
        "resetpass_forbidden": "Şîfre nikarin werin guhertin",
        "resetpass-submit-loggedin": "Şîfreyê biguherîne",
        "resetpass-submit-cancel": "Betal bike",
+       "resetpass-recycled": "Ji kerema xwe şîfreya xwe ya mewcûd biguherîne şîfreyeke din.",
        "resetpass-temp-password": "Şîfreya demkî:",
        "resetpass-expired": "Wextê şîfreya te derbas bû. Ji kerema xwe re\nji bo têketinê şîfreyek nû çêke.",
-       "resetpass-expired-soft": "Wextê şîfreya te derbas bû û hewceye ko be guhertin. Ji kerema xwe re şîfreyek nû çêke an jî ji bo paşî çêkî li \"{{int:authprovider-resetpass-skip-label}}\" bitikîne.",
-       "resetpass-validity-soft": "Şîfreya te derbasdar nîne: $1\n\nJi kerema xwe re şîfreyek nû çêke an jî ji bo paşî çêkî li \"{{int:authprovider-resetpass-skip-label}}\" bitikîne.",
+       "resetpass-expired-soft": "Mideta şîfreya te xelas bûye û divê bê guhertin. Ji kerema xwe re şîfreyeke nû çêbike an jî ji bo ku paşiyê biguherîne li \"{{int:authprovider-resetpass-skip-label}}\" bitikîne.",
+       "resetpass-validity-soft": "Şîfreya te ne derbasdar e: $1\n\nJi kerema xwe re şîfreyek nû çêke an jî ji bo paşiyê biguherîne li \"{{int:authprovider-resetpass-skip-label}}\" bitikîne.",
        "passwordreset": "Şîfreyê nû bike",
        "passwordreset-text-one": "Ji bo ji nû ve sazkirina şîfreyê vê formê dagire.",
        "passwordreset-username": "Navê bikarhêner:",
        "recentchanges-submit": "Nîşan bide",
        "rcfilters-legend-heading": "<strong>Lîsteya kurtenavan:</strong>",
        "rcfilters-activefilters": "Parzûnên çalak",
+       "rcfilters-activefilters-hide": "Veşêre",
+       "rcfilters-activefilters-show": "Nîşan bide",
        "rcfilters-days-show-days": "{{PLURAL:$1|rojek|$1 roj}}",
        "rcfilters-days-show-hours": "{{PLURAL:$1|saetek|$1 saet}}",
        "rcfilters-quickfilters": "Parzûnên tomarkirî",
        "rcfilters-savedqueries-new-name-label": "Nav",
        "rcfilters-savedqueries-cancel-label": "Betal bike",
        "rcfilters-filterlist-title": "Parzûn",
+       "rcfilters-filterlist-feedbacklink": "Ji me re biêje ka tu li ser van alavên fîltrekirinê çi difikirî.",
        "rcfilters-filter-editsbyself-label": "Guhêrandinên ji aliyê te",
        "rcfilters-filter-editsbyother-label": "Guherandinên ji aliyên kesên din",
        "rcfilters-filter-bots-label": "Bot",
        "whatlinkshere-title": "Rûpelên ku yê berve \"$1\" tên",
        "whatlinkshere-page": "Rûpel:",
        "linkshere": "Ev rûpel tên ser vê rûpelê '''$2''':",
-       "nolinkshere": "Ne ji rûpelekê lînk tên ser '''$2'''.",
+       "nolinkshere": "Girêdan bo <strong>$2</strong> nehat dîtin.",
        "nolinkshere-ns": "Ne lînkek berve '''$2''' di vê namespace'a da tê.",
        "isredirect": "rûpelê beralî bike",
        "istemplate": "tê bikaranîn",
index cb6a15f..4ec03ed 100644 (file)
        "showpreview": "Ингкъарав",
        "showdiff": "Тюзлевлени гёрсетмек",
        "anoneditwarning": "<strong>Тергев:</strong> Сен гириш этмединг. Тюзлевлер этсенг сени IP адресинг публикли гёрюнер. Эгер <strong>[$1 гириш]</strong> яда <strong>[$2 къайыт]</strong> этсенг, тюзлевлеринг сени ортакъчы аты булан гьисап этилер, оьзге пайдалардан да къайры.",
-       "blockedtext": "Сени ÐºÑ\8aоллавÑ\87Ñ\83 Ð°Ñ\82Ñ\8bнг Ñ\8fда IP Ð°Ð´Ñ\80еÑ\81инг ÐºÑ\8aамалгÑ\8aан Ñ\8dди.''\n\nÐ\9aÑ\8aамав Ñ\8dÑ\82ген $1 ($2).\nСебеп Ð±ÐµÑ\80илген: ''$3'.\n\n* Ð\9aÑ\8aамав Ð±Ð°Ñ\88ладÑ\8b: $4\n* Ð\9aÑ\8aамав Ð±Ð¸Ñ\82е: $5\n* Ð\9aÑ\8aамавнÑ\83 Ð¼Ñ\83Ñ\80адÑ\8b: $7\n\n$1 Ð±Ñ\83лан Ñ\8fда ÐºÑ\8aайÑ\81Ñ\8b Ð¾Ñ\8cзге [[{{MediaWiki:Grouppage-sysop}}|админиÑ\81Ñ\82Ñ\80аÑ\82оÑ\80]] Ð±Ñ\83лан ÐºÑ\8aаÑ\82нап ÐºÑ\8aамавнÑ\83 Ð³Ñ\8cакÑ\8aÑ\8bнда Ñ\81Ñ\91йлеÑ\88меге Ð±Ð¾Ð»Ð°Ñ\81ан.\nТеÑ\80геп ÐºÑ\8aой, Ñ\81ени [[Special:Preferences|Ñ\8dнÑ\87или ÐºÑ\8eйлемлеÑ\80ингде]] e-mail Ð°Ð´Ñ\80еÑ\81инг Ñ\82Ñ\8eз Ð±ÐµÑ\80мединг Ð±Ñ\83Ñ\81а Ñ\8fда Ð³ÐµÑ\80Ñ\82и Ñ\8dÑ\82мединг Ð±Ñ\83Ñ\81а, Ñ\8fда ÐºÑ\8aамав Ñ\88аÑ\80Ñ\82лагÑ\8aа Ð¼Ð°ÐºÑ\82Ñ\83п Ñ\8fзÑ\8bвÑ\83нÑ\83 ÐºÑ\8aадагÑ\8aа Ð³Ð¸Ñ\80е Ð±Ñ\83Ñ\81а, \"кÑ\8aоллавÑ\87Ñ\83гÑ\8aа Ð¼Ð°ÐºÑ\82Ñ\83п\" Ñ\84Ñ\83нкÑ\86иÑ\8fнÑ\8b ÐºÑ\8aоллап Ð±Ð¾Ð»Ð¼Ð°Ñ\81Ñ\81ан.\nСени IP Ð°Ð´Ñ\80еÑ\81инг â\80\94 $3, ÐºÑ\8aамавнÑ\83 Ð¸Ð´ÐµÐ½Ñ\82иÑ\84икаÑ\82оÑ\80Ñ\83 â\80\94 $5. Ð¢Ð¸Ð»ÐµÐ², Ð±Ñ\83 Ð¼Ð°Ñ\8aлÑ\8eмаÑ\82ланÑ\8b Ð±Ð°Ñ\80Ñ\8b Ñ\82алаплаÑ\80Ñ\8bнга Ð³Ð¸Ð¹Ð¸Ñ\80.",
+       "blockedtext": "Къамав этген $1 ($2).\nСебеп берилген: ''$3'.\n\n* Къамав башлады: $4\n* Къамав бите: $5\n* Къамавну мурады: $7\n\n$1 булан яда къайсы оьзге [[{{MediaWiki:Grouppage-sysop}}|администратор]] булан къатнап къамавну гьакъында сёйлешмеге боласан.\nТергеп къой, сени [[Special:Preferences|энчили кюйлемлерингде]] e-mail адресинг тюз бермединг буса яда герти этмединг буса, яда къамав шартлагъа мактуп язывуну къадагъа гире буса, \"къоллавчугъа мактуп\" функцияны къоллап болмассан.\nСени IP адресинг — $3, къамавну идентификатору — $5. Тилев, бу маълюматланы бары талапларынга гийир.",
        "loginreqlink": "гирмек",
        "newarticle": "(Янгы)",
        "newarticletext": "Сен гьалиде яратылмагъан сагьифагъа гёчдинг.\nБу сагьифаны яратмакъ учун, тюбюндеги къутугъунда язып башлагъыз (артыкъ маълюмат учун [$1 кёмек сагьифагъа] къара).\nЯнгылыш этип бери гёчген бусанг, сени браузеринг <strong>артгъа</strong> тюймесине бас.",
        "recentchangeslinked-feed": "Байлавлу тюзлевлер",
        "recentchangeslinked-toolbox": "Байлавлу тюзлевлер",
        "recentchangeslinked-title": "\"$1\" сагьифагъа байлавлу тюзлевлер",
-       "recentchangeslinked-summary": "Ð\91Ñ\83 Ñ\81агÑ\8cиÑ\84агÑ\8aа Ð±Ð°Ð¹Ð»Ð°Ð²Ð»Ñ\83 Ñ\82Ñ\8eзлевлеÑ\80и Ð±Ñ\83лан Ñ\81агÑ\8cиÑ\84аланÑ\8b, Ñ\8fда Ð¾ Ñ\81агÑ\8cиÑ\84адан Ð±Ð°Ð¹Ð»Ð°Ð²Ð»Ñ\83ланÑ\8b Ð³Ñ\91Ñ\80мек Ñ\83Ñ\87Ñ\83н атын яз. (Категорияны ортакъчаларын гёрмек учун Category:Категорияны атын яз). Сени [[Special:Watchlist|Гьызарлав тизменгдеги]] алышынывлар <strong>къалын</strong> гьарплылар.",
+       "recentchangeslinked-summary": "СагÑ\8cиÑ\84агÑ\8aа Ñ\8fда Ñ\81агÑ\8cиÑ\84адан Ð±Ð°Ð¹Ð»Ð°Ð²Ð»Ñ\83 Ñ\81агÑ\8cиÑ\84алаÑ\80да Ñ\82Ñ\8eзлевлеÑ\80ин Ð³Ñ\91Ñ\80мек Ñ\83Ñ\87Ñ\83н Ð¾Ð½Ñ\83 атын яз. (Категорияны ортакъчаларын гёрмек учун Category:Категорияны атын яз). Сени [[Special:Watchlist|Гьызарлав тизменгдеги]] алышынывлар <strong>къалын</strong> гьарплылар.",
        "recentchangeslinked-page": "Сагьифаны аты:",
        "recentchangeslinked-to": "Къайта, бу сагьифагъа байлавлу сагьифаланы алышынывларын гёрсетмек",
        "upload": "Сапламны юклемек",
        "sp-contributions-toponly": "Янгыз ахырынчы тюрню тюзлевлерин гёрсетмек",
        "sp-contributions-newonly": "Янгыз сагьифа яратыв деген тюзлевлени гёрсетмек",
        "sp-contributions-submit": "Излев",
-       "whatlinkshere": "Ð\9cÑ\83нда байланылгъан",
+       "whatlinkshere": "Ð\91еÑ\80и байланылгъан",
        "whatlinkshere-title": "\"$1\" бетге байлангъан сагьифалар",
        "whatlinkshere-page": "Сагьифа:",
        "linkshere": "Гелеген сагьифалар бугъар байлавлу <strong>$2</strong>:",
index 089614a..74001ee 100644 (file)
        "resetpass-temp-password": "Temporäert Passwuert:",
        "resetpass-abort-generic": "D'Ännere vum Passwuert gouf duerch eng Erweiderung ofgebrach.",
        "resetpass-expired": "Äert Passwuert ass ofgelaf. Gitt w.e.g. en neit Passwuert u fir Iech anzeloggen.",
-       "resetpass-expired-soft": "Äert Passwuert ass ofgelaf a muss zeréckgesat. Sicht w.e.g. elo en neit Passwuert eraus oder klickt  \"{{int:authprovider-resetpass-skip-label}}\" fir et spéider zeréckzesetzen.",
-       "resetpass-validity-soft": "Ärt Passwuert ass net valabel: $1",
+       "resetpass-expired-soft": "Äert Passwuert ass ofgelaf a muss geännert ginn. Sicht w.e.g. elo en neit Passwuert eraus oder klickt  \"{{int:authprovider-resetpass-skip-label}}\" fir et spéider z'änneren.",
+       "resetpass-validity-soft": "Äert Passwuert ass net valabel: $1\n\nWielt elo en neit Passwuert oder klickt op \"{{int:authprovider-resetpass-skip-label}}\" fir et méi spéit z'änneren.",
        "passwordreset": "Passwuert zrécksetzen",
        "passwordreset-text-one": "Fëllt dëse Formulaire aus fir Äert Passwuert zréckzesetzen.",
        "passwordreset-text-many": "{{PLURAL:$1|Fëllt eent vun de Felder aus fir en temporäert Passwuert per E-Mail ze kréien.}}",
        "rcfilters-other-review-tools": "Aner Méiglechkeete fir z'iwwerliesen",
        "rcfilters-group-results-by-page": "Resultater no de Säite gruppéieren",
        "rcfilters-activefilters": "Aktiv Filteren",
+       "rcfilters-activefilters-hide": "Verstoppen",
+       "rcfilters-activefilters-show": "Weisen",
        "rcfilters-advancedfilters": "Erweidert Filteren",
        "rcfilters-limit-title": "Resultater fir ze weisen",
        "rcfilters-limit-and-date-label": "{{PLURAL:$1|Eng Ännerung|$1 Ännerungen}}, $2",
        "rcfilters-savedqueries-rename": "Ëmbenennen",
        "rcfilters-savedqueries-setdefault": "Als Standard festleeën",
        "rcfilters-savedqueries-unsetdefault": "Als Standard ewechhuelen",
-       "rcfilters-savedqueries-remove": "Ewechhuelen",
+       "rcfilters-savedqueries-remove": "Läschen",
        "rcfilters-savedqueries-new-name-label": "Numm",
        "rcfilters-savedqueries-new-name-placeholder": "Den Zweck vum Filter beschreiwen",
        "rcfilters-savedqueries-apply-label": "Filter uleeën",
        "rcfilters-empty-filter": "Keen aktive Filter. All Kontributioune gi gewisen.",
        "rcfilters-filterlist-title": "Filteren",
        "rcfilters-filterlist-whatsthis": "Wéi geet dat?",
-       "rcfilters-filterlist-feedbacklink": "Sot eis wat Dir vun dësen (neien) Filterméiglechkeeten haalt",
+       "rcfilters-filterlist-feedbacklink": "Sot eis wat Dir vun dësen Filterméiglechkeeten haalt",
        "rcfilters-highlightbutton-title": "Resultater ervirhiewen",
        "rcfilters-highlightmenu-title": "Eng Faarf eraussichen",
        "rcfilters-highlightmenu-help": "Sicht eng Faarf eraus fir dës Eegenschaft ervirzehiewen.",
index cbd0118..5b2b669 100644 (file)
        "subject-preview": "Temos peržiūra:",
        "previewerrortext": "Įvyko klaida bandant peržiūrėti jūsų pakeitimus.",
        "blockedtitle": "Naudotojas yra užblokuotas",
-       "blockedtext": "'''Jūsų naudotojo vardas arba IP adresas yra užblokuotas.'''\n\nUžblokavo $1. Nurodyta priežastis yra ''$2''.\n\n* Blokavimo pradžia: $8\n* Blokavimo pabaiga: $6\n* Užblokuotasis: $7\n\nJūs galite susisiekti su $1 arba kuriuo nors kitu [[{{MediaWiki:Grouppage-sysop}}|administratoriumi]] ir aptarti neaiškumus dėl blokavimo.\nAtkreipkite dėmesį, kad negalėsite naudotis funkcija „Rašyti laišką šiam naudotojui“, jei nesate užsiregistravę ir pateikę galiojančio el. pašto adreso naudotojo paskyros [[Special:Preferences|nustatymuose]], arba, jei jums užblokuotas šios funkcijos naudojimas.\nJūsų IP adresas yra $3, o blokavimo ID yra #$5.\nPrašome nurodyti vieną iš jų ar abu, kai kreipiatės dėl blokavimo.",
+       "blockedtext": "'''Jūsų naudotojo vardas arba IP adresas yra užblokuotas.'''\n\nUžblokavo $1. Nurodyta priežastis yra ''$2''.\n\n* Blokavimo pradžia: $8\n* Blokavimo pabaiga: $6\n* Užblokuotasis: $7\n\nJūs galite susisiekti su $1 arba kuriuo nors kitu [[{{MediaWiki:Grouppage-sysop}}|administratoriumi]] ir aptarti neaiškumus dėl blokavimo.\nAtkreipkite dėmesį, kad negalėsite naudotis funkcija „{{int:emailuser}}“, jei nesate užsiregistravę ir pateikę galiojančio el. pašto adreso naudotojo paskyros [[Special:Preferences|nustatymuose]], arba, jei jums užblokuotas šios funkcijos naudojimas.\nJūsų IP adresas yra $3, o blokavimo ID yra #$5.\nPrašome nurodyti vieną iš jų ar abu, kai kreipiatės dėl blokavimo.",
        "autoblockedtext": "Jūsų IP adresas buvo automatiškai užblokuotas, nes jį naudojo kitas naudotojas, kurį užblokavo $1.\nNurodyta priežastis yra ši:\n\n:''$2''\n\n* Blokavimo pradžia: $8\n* Blokavimo pabaiga: $6\n* Numatomas blokavimo laikas: $7\n\nJūs galite susisiekti su $1 arba kitu [[{{MediaWiki:Grouppage-sysop}}|administratoriumi]], kad aptartumėte neaiškumus dėl blokavimo.\n\nJūs negalite naudotis funkcija „Rašyti laišką šiam naudotojui“, jei nesate nurodę tikro el. pašto adreso savo [[Special:Preferences|naudotojo nustatymuose]]. Taip pat Jūs negalite naudotis šia funkcija, jei Jums užblokuotas jos naudojimas.\n\nJūsų IP adresas yra $3, blokavimo ID yra $5.\nPrašome nurodyti šiuos duomenis visais atvejais, kai kreipiatės dėl blokavimo.",
        "blockednoreason": "priežastis nenurodyta",
        "whitelistedittext": "Jūs turite $1, kad redaguotumėte puslapius.",
index 62bdb7d..7138233 100644 (file)
        "botpasswords-created-title": "Bota parole izveidota",
        "botpasswords-updated-title": "Bota parole atjaunināta",
        "botpasswords-deleted-title": "Bota parole dzēsta",
+       "botpasswords-restriction-failed": "Botu paroles ierobežojumi liedz šo pieslēgšanos.",
        "resetpass_forbidden": "Paroles nav iespējams nomainīt",
        "resetpass_forbidden-reason": "Paroles nav iespējams nomainīt: $1",
        "resetpass-no-info": "Jums ir nepieciešams ieiet, lai tūlīt piekļūtu šai lapai.",
        "rcfilters-filter-minor-description": "Labojumi, kas atzīmēti kā maznozīmīgi.",
        "rcfilters-filter-major-label": "Nozīmīgi labojumi",
        "rcfilters-filter-major-description": "Labojumi, kas nav atzīmēti kā maznozīmīgi.",
+       "rcfilters-filtergroup-watchlist": "Uzraugāmie raksti",
        "rcfilters-filter-watchlist-watchednew-description": "Izmaiņas uzraugāmajās lapās, kuras nav apmeklētas kopš izmaiņu veikšanas.",
        "rcfilters-filter-watchlistactivity-unseen-label": "Neapskatītas izmaiņas",
        "rcfilters-filter-watchlistactivity-unseen-description": "Izmaiņas lapās, kuras nav apmeklētas kopš izmaiņu veikšanas.",
index eecf9e5..6337ea4 100644 (file)
        "redirectedfrom": "(tonga teto avy amin'ny $1)",
        "redirectpagesub": "Pejy fihodinana",
        "redirectto": "Mihodina mankany:",
-       "lastmodifiedat": "Voaova farany tamin'ny $1 amin'ny $2 ity pejy ity<br />",
+       "lastmodifiedat": "Voaova farany tamin'ny $1 amin'ny $2 ity pejy ity.",
        "viewcount": "voastsidika in-$1 ity pejy ity.{{PLURAL:}}",
        "protectedpage": "Pejy voaaro",
        "jumpto": "Hanketo:",
        "nosuchusershort": "Tsy misy mpikambana hoe \"$1\". Hamarino ny tsipelina.",
        "nouserspecified": "Tsy maintsy mampiditra solonanarana ianao.",
        "login-userblocked": "Voasakana io mpikambana io. Fidirana tsy nahazoan-dalana.",
-       "wrongpassword": "Diso ny tenimiafina. Manandrama tenimiafina hafa azafady.",
+       "wrongpassword": "Diso ny tenimiafina. \nAndramo indray azafady.",
        "wrongpasswordempty": "Tsy nampiditra tenimiafina ianao, azafady mba avereno indray.",
        "passwordtooshort": "{{PLURAL:}}Fohy loatra io tenimiafina io.\nFarafahakeliny tokony hisy litera $1 ny tenimiafina.",
        "passwordtoolong": "Tsy azo atao ho lava noho ny soratra {{PLURAL:$1|iray|$1}} ny tenimiafina.",
-       "passwordtoopopular": "Tsy azo ampiasana ny tenimiafina ampiasaina matetika. Misafidiana tenimiafina manokana kokoa.",
+       "passwordtoopopular": "Tsy azo ampiasana ny tenimiafina ampiasaina matetika. Misafidiana tenimiafina sarotra vinavinaina kokoa.",
        "password-name-match": "Tsy maintsy samihafa ny solonanaranao sy ny tenimiafinao tompoko.",
        "password-login-forbidden": "Norarana ny fampiasana io anaram-pikambana ary io tenimiafina io.",
        "mailmypassword": "Hamerina ny tenimiafina",
        "passwordremindertitle": "Fampatsiahivana tenimiafina avy amin'i {{SITENAME}}",
-       "passwordremindertext": "Nisy olona, izay ianao ihany angamba, avy tamin'ny adiresy IP $1, nangataka\nny handefasanay tenimiafina vaovao ho an'ny sehatra {{SITENAME}} ao amin'ny\n$4.\nLasa \"$3\" ankehitriny ny tenimiafin'i \"$2\"\nAfaka miditra ary ianao ankehitriny ary manova ny tenimiafinao.\nLany andro anatin'ny $5 andro ny tenimiafinao\n\nRaha olon-kafa io nangataka io, na tadidinao ihany ny tenimiafinao taloha ka\ntsy irinao hovana intsony, dia fafao fotsiny ity hafatra ity dia ilay\ntenimiafina taloha ihany no ampiasao.{{PLURAL:}}",
+       "passwordremindertext": "Nisy olona (avy amin'ny adiresy IP $1) nangataka tenimiafina vaovao ho an'i {{SITENAME}} ($4). Noforonina ny tenimiafina vonjimaika ho an'ny mpikambana \"$2\" ary natao ho \"$3\". Raha ny finiavanao no nahatonga  izany, dia mila miditra ianao ary misafidy tenimiafina vaovao dien'izao.\nHitsahatra afaka {{PLURAL:$5|iray andro|$5 andro}} ny tenimiafinao.\n\nRaha olon-kafa no nanao ity hataka ity, na efa tadidinao indray ny tenimiafinao ka tsy te-hanova azy intsony, dia azonao tsy raharahiana ity hafatra ity ary azonao ampiasaina ilay tenimiafinao taloha.",
        "noemail": "Tsy nanome adiresy imailaka i \"$1\".",
        "noemailcreate": "Tsy maintsy misy ny adiresy imailaka ho atsofokao",
        "passwordsent": "Nandefasana tenimiafina vaovao any amin'ny adiresy imailak'i \"$1\".\nAzafady midira rehefa voarainao io imailaka io.",
        "botpasswords-insert-failed": "Tsy afaka nanampy ny anarana rôbô \"$1\". Tsy efa nampiana ve ilay izy?",
        "botpasswords-update-failed": "Tsy afaka nanavao ny anarana rôbô \"$1\". Nofafàna ve ilay izy?",
        "botpasswords-created-title": "Noforonina ilay tenimiafina rôbô",
-       "botpasswords-created-body": "Noforonina ny tenimiafina rôbô \"$1\" an'ny mpikambana \"$2\".",
+       "botpasswords-created-body": "Noforonina ny tenimiafina rôbô \"$1\"{{GENDER:$2|}} an'ny mpikambana \"$2\".",
        "botpasswords-updated-title": "Nohavaozina ny tenimiafina rôbô",
-       "botpasswords-updated-body": "Nohavaozina ny tenimiafina rôbô \"$1\" an'ny mpikambana \"$2\".",
+       "botpasswords-updated-body": "Nohavaozina ny tenimiafina rôbô \"$1\" an'ny mpikambana \"$2\"{{GENDER:$2|}}.",
        "botpasswords-deleted-title": "Nofafàna ny tenimiafina rôbô",
-       "botpasswords-deleted-body": "Nofafàna ny tenimiafina rôbô \"$1\" an'ny mpikambana \"$2\".",
-       "botpasswords-newpassword": "Ny tenimiafina idirana amin'i <strong>$1</strong> dia <strong>$2</strong>. <em>Raiketo ilay izy ho an'ny fampiasàna any aoriana any.</em>",
+       "botpasswords-deleted-body": "Nofafàna ny tenimiafina rôbô \"$1\" an'ny mpikambana \"$2{{GENDER:$2|}}\".",
+       "botpasswords-newpassword": "Ny tenimiafina hidirana amin'i <strong>$1</strong> dia <strong>$2</strong>. <em>Raiketo izany ho fampatsiahivana ho an'ny ho avy</em><br>(Ho an'ny rôbô taloha izay mila ny anaram-pidirana mitovy amin'ny anaram-pikambana dia azonao ampiasaina ho anaram-pikambana i <strong>$3</strong> ary niy tenimiafina <strong>$4</strong>.)",
        "botpasswords-no-provider": "Tsy afaka antsoina ny BotPasswordsSessionProvider.",
        "botpasswords-restriction-failed": "Manakana ity fidirana ity ny fepetra mifehy ny tenimiafina rôbô.",
        "resetpass_forbidden": "Tsy afaka ovaina ny tenimiafina",
        "resetpass-submit-loggedin": "Ovay ny tenimiafina",
        "resetpass-submit-cancel": "Aoka ihany",
        "resetpass-wrong-oldpass": "Tsy izy ny tenimiafinao (ankehitriny na vonjimaika)\nMety efa nanova tenimiafina na nangataka hahazo tenimiafina vonjimaika angamba ianao.",
-       "resetpass-recycled": "Avereno amy zavatra hafa nohon'ny tenimiafinao ankehitriny ny tenimiafinao.",
+       "resetpass-recycled": "Ovay ho zavatra samihafa amin'ny tenimiafinao ankehitriny ny tenimiafinao.",
        "resetpass-temp-emailed": "Niditra tamin'ny alalan'ny tenimiafina vonjimaika nalefa mailaka ianao.\nMba hamaranana ny fidirana, dia tsy maintsy mampiditra tenimiafina vaovao eto ianao :",
        "resetpass-temp-password": "Tenimiafina miserana :",
        "resetpass-abort-generic": "Nosakanan'ny itatra (extension) iray ny fanovana tenimiafina.",
        "resetpass-expired": "Efa nitsahatra ny tenimiafinao. Mampidira tenimiafina vaovao hahafahanao miditra.",
-       "resetpass-expired-soft": "Efa nitsahatra ny tenimiafinao, ary tsy maintsy averina ilay izy. Safidio avy hatrany ny tenimiafina, na tsindrio \"{{int:authprovider-resetpass-skip-label}}\" raha tsy hanao izany androany",
-       "resetpass-validity-soft": "Tsy ekena ny tenimiafinao : $1\n\nTenimiafina vaovao fidiana, na tsindrio \"{{int:authprovider-resetpass-skip-label}}\" raha hamerina azy amin'ny fotoana hafa.",
+       "resetpass-expired-soft": "Efa nitsahatra ny tanimiafinao ka mila ovaina. Misafidiana tenimiafina vaovao dien'izao, na tsindrio \"{{int:authprovider-resetpass-skip-label}}\" raha hanova azy amin'ny fotoana hafa.",
+       "resetpass-validity-soft": "Tsy ekena ny tenimiafinao : $1\n\nMisafidiana tenimiafina vaovao izao, na tsindrio  \"{{int:authprovider-resetpass-skip-label}}\" raha te-hanova azy any aoriana.",
        "passwordreset": "Famafana ary famerenana ny tenimiafina",
        "passwordreset-text-one": "Fenoy ity fôrmiolera ity mba hamerenana ny tenimiafinao",
        "passwordreset-text-many": "{{PLURAL:$1|Fenoy ny saha mba hahazoanao tenimiafina vonjimaika.}}",
        "passwordreset-emailtext-ip": "Nisy olona (izay mety ianao, avy amin'ny adiresy IP $1) nangataka ny hamerina ny tenimiafin'ny kaontim-pikambany ho an'i {{SITENAME}} ($4). Mampiasa ity adiresy mailaka ity {{PLURAL:$3|ity kaontim-pikambana mpikambana io|ireo kaontim-mpikambana ireo}}:\n\n$2\n\nHitsahatra afaka $5 andro {{PLURAL:$3|io tenimiafina io|ireo tenimiafina ireo}}.\nTokony miditra ianao ary mifidy tenimiafina vaovao. Raha misy olon-kafa nanao ity hataka ity, na efa tadidinao indray ilay tenimiafinao taloha, ary raha tsy tia hanova azy intsony, azonao tsy raharahiana ity hafatra ity ary mitohy mampiasa ny tenimiafinao taloha.",
        "passwordreset-emailtext-user": "Nisy mpikambana mitondra anarana $1 eo amin'i {{SITENAME}} nangataka fampatsiahivana mikasika ny kaontinao eo amin'i {{SITENAME}} ($4). Manana io adiresy imailaka {{PLURAL:$3|io kaontim-pikambana io|ireo kaontim-pikambana ireo}} :\n\n$2\n\nHitsahatra afaka {{PLURAL:$5|iray|$5}} andro {{PLURAL:$3|io|ireo}} tenimiafina {{PLURAL:$3|io|ireo}}. Mila miditra dien'izao ianao izao ary mifidy tenimiafina vaovao. Raha tsy avy aminao ity hataka ity na efa nahatadidy ny tenimiafinao taloha ianao, ary raha tsy tianao hovaina intsony ilay tenimiafinao, dia azonao tsy raharahiana ity hafatra ity ary mampiasa ny tenimiafinao taloha.",
        "passwordreset-emailelement": "Anaram-pikambana : \n$1\n\nTenimiafina miserana : \n$2",
-       "passwordreset-emailsentemail": "Lasa ny mailaka famerenana tenimiafina.",
+       "passwordreset-emailsentemail": "Raha miaraka amin'ny kaontinao ity adiresy mailaka ity, dia ho alefa ny mailaka famerenan-tenimiafina.",
        "passwordreset-nocaller": "Mila manitsy mpiantso",
        "passwordreset-nosuchcaller": "Tsy misy ilay mpiantso: $1",
        "passwordreset-invalidemail": "Adiresy mailaka tsy azo raisina",
        "preview": "Topi-maso",
        "showpreview": "Asehoy aloha",
        "showdiff": "Asehoy ny fiovana",
-       "blankarticle": "<strong>Tandremo:</strong> Fotsy ny pejy tianao hoforonina.\nRaha manindry an'i \"$1\" indray ianao dia hoforonina tsy hisy vontoatiny na inona na innona ilay pejy.",
+       "blankarticle": "<strong>Tandremo:</strong> Fotsy ny pejy tianao hoforonina.\nRaha manindry an'i \"$1\" indray ianao dia hoforonina tsy hisy vontoatiny na inona na inona ilay pejy.",
        "anoneditwarning": "<strong>Fampitandremana :</strong> Tsy niditra tamin'ny kaontinao ianao. Ho hitan'ny vahoaka ny adiresy IP-nao raha manova inona na inona ianao. Raha <strong>[$1 miditra amin'ny kaontinao]</strong> ianao dia ho anisan'ny tombontsoa anananao ny fanaovana ny fiovana amin'ny solonanaranao.",
        "anonpreviewwarning": "''Tsy niditra ianao. Hampitahiry ny adiresy IP anao ao amin'ny tantaram-panovan'ity pejy ity ny fitehirizana ny fanovana.''",
        "missingsummary": "'''Hafatra fampantsiahivana''' : tsy mbola nanome ny ambangovangom-panovanao ianao.\nRaha mbola tsindriano fanindroany eo amin'ny bokotra $1, ho voatahiry tsy fanambarana ny fanovanao.",
        "missingcommenttext": "Ampidiro ny ambangovangony azafady.",
-       "missingcommentheader": "'''Fampahatsiahivana :''' Tsy nampiditra lohateny amin'ity resaka ity ianao.\nRaha tsindrianao indray eo amin'ny « {{MediaWiki:Savearticle}} » ho voatahiry tsy misy lohateny ny fanovananao.",
-       "summary-preview": "Topi-maso n'ilay ambangovangony :",
-       "subject-preview": "Topi maso ny lazaina :",
+       "missingcommentheader": "<strong>Fampatsiahivana: </strong> Tsy nampiditra lohahevitra ho an'ity hafatra ity ianao. Raha tsindrianao fanindroany \"$1\" dia ho tehirizina tsy misy lohahevitra ny hafatrao.",
+       "summary-preview": "Topi-mason'ny ambangovangom-panovana :",
+       "subject-preview": "Topi-mason-dohahevitra:",
        "previewerrortext": "Nisy hadisoana nitranga tamin'ny fanandramana namoaka topi-mason'ny fanovanao",
        "blockedtitle": "Mpikambana voasakana",
-       "blockedtext": "'''Voasakana ny solonanaranao na ny adiresy IP anao.'''\n\nNataon'i $1 ny fisakanana.\nNy antony : ''$2''.\n\n* Fanombohan'ilay fisakanana : $8\n* Farany : $6\n* Kaonty voasakana : $7.\n\nAfaka antsoinao i $1 na [[{{MediaWiki:Grouppage-sysop}}|ny mpandrindra]] mba hiresaka mombamomba n'izany.\nAfaka andefasanao imailaka ra nampiditra ny adiresy imailakanao ianao ao anatin'ny [[Special:Preferences|mombamombanao]].\n'''$3''' ny adiresy IP-nao ary ny ''identifiant de blocage''-nao dia #$5.\nAsio ao anaty ny fangatahanao io adiresy io.",
-       "autoblockedtext": "Voasakana ny adiresy IP anareo satria nampiasain'ny olon-kafa io adiresy ampiasainao io. Ary voasakan'i $1 ilay olona nampiasa ny adiresinao.<br />\nIty ny antony navoakany\n\n:''$2''\n\n* nanomboka tamin'ny $8 ilay fisakanana\n* Amin'ny $6 ilay fisakanana no mijanona\n* $7 no anaran'ilay kaonty voasakana\n\nAfaka antsoinao i $1 na miantso ny [[{{MediaWiki:Grouppage-sysop}}|mpandrindra]] mba hiresaka momba ny fanakananao.\n\nJereo koa fa tsy afaka mampiasa ny asa ''emailuser'' ianao ra tsy nanometraka ny adiresy imailakao anatin'ny [[Special:Preferences|safidinao]]. Jereo koa ra tsy nesorinao ny asa ''emailuser''.\n\n$3 izao ny adiresinao, ary ny isa ny fisakananai dia $5.\nSoraty ireo fanoroana ireo anatin'ny fangatahana ataonao.",
+       "blockedtext": "<strong>Voasakana ny adiresy IP-nao na ny anaram-pikambanao.</strong>\n\nI $1 no nanao ny sakana.\nNy antony nomeny dia <em>$2</em>.\n\n* Fiantombohan'ny sakana: $8\n* Fitsaharan'ny sakana: $6\n* Ny iriana ho sakanana: $7\n\nAzonao atao ny mifandray amin'i $1 na [[{{MediaWiki:Grouppage-sysop}}|mpandrindra]] hafa raha mila miady hevitra momba ny sakana.\nTsy afaka mampiasa ny fahafahana \"{{int:emailuser}}\" ianao raha tsy nanome adiresy azo anoratana anao ao amin'ny [[Special:Preferences|safidin'ny kaontinao]] ary koa raha tsy nosakanana tsy afaka mampiasa azy ianao.\nNy adiresy IP-nao dia $3 ary ny ID ny sakana dia #$5.\nSoraty avokoa ireo antsipirihany eo ambony ireo anatin'ny hataka izay ataonao.",
+       "autoblockedtext": "<strong>Voasakana ny adiresy IP-nao satria nampiasaim-pikambana hafa izay nosakanan'i $1 ilay izy.</strong>\n\n\nNy antony nomeny dia:\n:<em>$2</em>.\n\n* Fiantombohan'ny sakana: $8\n* Fitsaharan'ny sakana: $6\n* Ny iriana ho sakanana: $7\n\nAzonao atao ny mifandray amin'i $1 na [[{{MediaWiki:Grouppage-sysop}}|mpandrindra]] hafa raha mila miady hevitra momba ny sakana.\nTsy afaka mampiasa ny fahafahana \"{{int:emailuser}}\" ianao raha tsy nanome adiresy azo anoratana anao ao amin'ny [[Special:Preferences|safidin'ny kaontinao]] ary koa raha tsy nosakanana tsy afaka mampiasa azy ianao.\nNy adiresy IP-nao dia $3 ary ny ID ny sakana dia #$5.\nSoraty avokoa ireo antsipirihany eo ambony ireo anatin'ny hataka izay ataonao.",
        "blockednoreason": "tsy nisy antony nomeny",
        "whitelistedittext": "Mila $1 aloha ianao vao afaka manova/mamorona pejy eto amin'ity wiki ity.",
        "confirmedittext": "Tsy maintsy marihina ny adiresy imailakao aloha no manova pejy.\nAmpidiro sy Checkeo ny adiresy imailakao amin'ny [[Special:Preferences|safidinao]].",
        "permissionserrorstext": "Tsy afaka manao ilay aza nangatahanao ianao noho ny antony {{PLURAL:$1||maro}} manaraka :",
        "permissionserrorstext-withaction": "{{PLURAL:$1|Tsy manana alalàna ianao|Tsy manana alalàna ianao}} $2. Io ny antony ($2):",
        "recreate-moveddeleted-warn": "'''Tandremo''' : Mamerina pejy efa voafafa ianareo.'''\n\nMarino raha tsara tohizana ny fanovana eto amin'ity pejy ity. Ny laogim-pamafana sy ny famindran-toerana dia eo ambany :",
-       "moveddeleted-notice": "Voafafa ity pejy ity.\nEo ambany eo any laogin'ny famindran-toerana sy ny famafana ho an'ny antsipirihany.",
+       "moveddeleted-notice": "Voafafa ity pejy ity.\nAseho ho  fampatsiahivana eo ambany eo any laogin'ny famindran-toerana sy ny famafana.",
        "log-fulllog": "Hijery ny laogy manontolo",
        "edit-hook-aborted": "Tsy nety ny fanovàna\nTsy nanome antony",
        "edit-gone-missing": "Tsy afaka natao update ilay pejy.\nMety voafafa angamba izy.",
        "page_first": "voalohany",
        "page_last": "farany",
        "histlegend": "Safidim-pahasamihafana: Mariho ireo bokotra radiôn'ny versiona mba hampitahàna azy ireo  ary tsindrony ilay bokotra amin'ny faran'ny pejy: <strong>({{int:cur}})</strong> = fampitahana amin'ny versiona farany indrindra, <strong>({{int:last}})</strong> = fahasamihafana amin'ny versiona farany nialoha ity, <strong>{{int:minoreditletter}}</strong> = fiovana madinika.",
-       "history-fieldset-title": "Karohy ny tantara",
+       "history-fieldset-title": "Hikaroka tantara",
        "history-show-deleted": "Voafafa ihany",
        "histfirst": "antitra indrindra",
        "histlast": "vaovao indrindra",
        "rcshowhidecategorization": "$1 ny fisokajiam-pejy",
        "rcshowhidecategorization-show": "Aseho",
        "rcshowhidecategorization-hide": "Afenina",
-       "rclinks": "Asehoy ny $1 niova farany tato anatin'ny $2 andro",
+       "rclinks": "Haneho ny fiovana $1 farany tanatin'ny andro $2 farany",
        "diff": "Fampitahana",
        "hist": "tant.",
        "hide": "Afeno",
        "recentchangeslinked-feed": "Novaina",
        "recentchangeslinked-toolbox": "Novaina",
        "recentchangeslinked-title": "Fanaraha-maso ny pejy miarak'amin'ny « $1 »",
-       "recentchangeslinked-summary": "Mampiseho ny fanovàna vao haingana ity pejy manokana ity. Voasoratra amin'ny '''sora-matavy''' ny lohaten'ny [[Special:Watchlist|pejy arahinao-maso]].",
+       "recentchangeslinked-summary": "Mampiseho ny fanovàna vao haingana ity pejy manokana ity. Voasoratra amin'ny '''sora-matavy''' ny lohaten'ny [[Special:Watchlist|pejy arahinao-maso]] dia hiseho amin'ny <strong>sora-baventy</strong>.",
        "recentchangeslinked-page": "anaram-pejy :",
        "recentchangeslinked-to": "Ampisehoy ny fanovàn'ny pejy misy rohy makany amin'ny pejy fa tsy atao mivadika",
        "recentchanges-page-added-to-category": "Nampiana tamin'ny sokajy [[:$1]]",
        "revdelete-restricted": "nametraka fanerena ho an'ny mpandrindra",
        "revdelete-unrestricted": "fanerena nesorina tamin'ny mpandrindra",
        "logentry-move-move": "nanova ny anaran'i $3 ho $4 i $1",
+       "logentry-move-move-noredirect": "$1{{GENDER:$2|}} dia namindra ny pejy $3 ho $4 fa tsy namela fihodinana",
        "logentry-newusers-newusers": "{{GENDER:$2|Noforonina}} ny kaontim-pikambana $1",
        "logentry-newusers-create": "{{GENDER:$2|Noforonina}} ny kaontim-pikambana $1",
        "logentry-newusers-create2": "{{GENDER:$2|Noforonin}}'i $1 ny kaomtim-pikambana $3",
index 6d7f64e..6283381 100644 (file)
        "resetpass-temp-password": "Привремена лозинка:",
        "resetpass-abort-generic": "Смената на лозинката е откажана од додаток.",
        "resetpass-expired": "Лозинката ви е истечена. Задајте нова лозинка за да се најавите.",
-       "resetpass-expired-soft": "Лозинката ви е истечена и ќе мора да зададете друга. Изберете ја сега, или пак стиснете на „{{int:authprovider-resetpass-skip-label}}“ за да ја зададете подоцна.",
-       "resetpass-validity-soft": "Лозинката ви е неважечка: $1\n\nИзберете друга сега, или пак стиснете на „{{int:authprovider-resetpass-skip-label}}“ за да ја зададете подоцна.",
+       "resetpass-expired-soft": "Лозинката ви е истечена и ќе мора да ја смените. Изберете ја сега, или пак стиснете на „{{int:authprovider-resetpass-skip-label}}“ за да ја смените подоцна.",
+       "resetpass-validity-soft": "Лозинката ви е неважечка: $1\n\nИзберете друга сега, или пак стиснете на „{{int:authprovider-resetpass-skip-label}}“ за да ја смените подоцна.",
        "passwordreset": "Менување на лозинка",
        "passwordreset-text-one": "Пополнете го образецов за да ја измените лозинката.",
        "passwordreset-text-many": "{{PLURAL:$1|Пополнете едно од полињата за добиете привремена лозинка по е-пошта.}}",
        "rcfilters-other-review-tools": "Други алатки за проверка",
        "rcfilters-group-results-by-page": "Групен исход по страница",
        "rcfilters-activefilters": "Активни филтри",
+       "rcfilters-activefilters-hide": "Скриј",
+       "rcfilters-activefilters-show": "Прикажи",
        "rcfilters-advancedfilters": "Напредни филтри",
        "rcfilters-limit-title": "Ставки за приказ",
        "rcfilters-limit-and-date-label": "$1 {{PLURAL:$1|промена|промени}}, $2",
        "rcfilters-savedqueries-rename": "Преименувај",
        "rcfilters-savedqueries-setdefault": "Задај како основно",
        "rcfilters-savedqueries-unsetdefault": "Отстрани од основно",
-       "rcfilters-savedqueries-remove": "Ð\9eÑ\82Ñ\81Ñ\82Ñ\80ани",
+       "rcfilters-savedqueries-remove": "Ð\98збÑ\80иÑ\88и",
        "rcfilters-savedqueries-new-name-label": "Назив",
        "rcfilters-savedqueries-new-name-placeholder": "Опишете ја намената на филтерот",
        "rcfilters-savedqueries-apply-label": "Создај филтер",
        "rcfilters-empty-filter": "Нема активни филтри. Прикажани се сите придонеси.",
        "rcfilters-filterlist-title": "Филтри",
        "rcfilters-filterlist-whatsthis": "Како работи ова?",
-       "rcfilters-filterlist-feedbacklink": "Дајте ни ваше мислење за овие (нови) филтерски алатки",
+       "rcfilters-filterlist-feedbacklink": "Дајте ни ваше мислење за овие филтерски алатки",
        "rcfilters-highlightbutton-title": "Истакнување на исход",
        "rcfilters-highlightmenu-title": "Изберете боја",
        "rcfilters-highlightmenu-help": "Изберете боја за да го истакнете ова својство",
        "rollback-success": "Откажани уредувањата на {{GENDER:$3|$1}};; вратено на последната верзија на {{GENDER:$4|$2}}.",
        "rollback-success-notify": "Откажани уредувањата на $1;\nвратено на последната преработка на $2. [$3 Пок. промени]",
        "sessionfailure-title": "Седницата не успеа",
-       "sessionfailure": "Ð\98ма Ð¿Ñ\80облем Ñ\81о Ð²Ð°Ñ\88аÑ\82а Ñ\81едниÑ\86а;\nоваа Ð´ÐµÑ\98Ñ\81Ñ\82во Ðµ Ð¾Ñ\82кажано ÐºÐ°ÐºÐ¾ Ð¿Ñ\80евенÑ\82ива Ð¿Ñ\80оÑ\82ив Ð¿Ñ\80еземаÑ\9aе Ñ\81едниÑ\86и.\nПоднесете го образецот повторно.",
+       "sessionfailure": "Се Ñ\98ави Ð¿Ñ\80облем Ñ\81о Ð½Ð°Ñ\98авнаÑ\82а Ñ\81едниÑ\86а;\nова Ð´ÐµÑ\98Ñ\81Ñ\82во Ðµ Ð¾Ñ\82кажано Ð·Ð° Ð´Ð° Ñ\81е Ñ\81пÑ\80еÑ\87и Ð½ÐµÑ\98зина ÐºÑ\80ажба.\nПоднесете го образецот повторно.",
        "changecontentmodel": "Промена на содржинскиот модел на страница",
        "changecontentmodel-legend": "Промена на содржински модел",
        "changecontentmodel-title-label": "Наслов на страницата",
index 9b2f488..61b733b 100644 (file)
        "rcfilters-savedqueries-rename": "പേരുമാറ്റുക",
        "rcfilters-savedqueries-setdefault": "സ്വതേയുള്ളതാക്കുക",
        "rcfilters-savedqueries-unsetdefault": "സ്വതേയുള്ളതെന്നത് ഒഴിവാക്കുക",
-       "rcfilters-savedqueries-remove": "à´¨àµ\80à´\95àµ\8dà´\95à´\82 à´\9aàµ\86à´¯àµ\8dà´¯ുക",
+       "rcfilters-savedqueries-remove": "മായàµ\8dà´\95àµ\8dà´\95ുക",
        "rcfilters-savedqueries-new-name-label": "പേര്‌",
        "rcfilters-savedqueries-new-name-placeholder": "അരിപ്പയുടെ ഉപയോഗം വിവരിക്കുക",
        "rcfilters-savedqueries-apply-label": "അരിപ്പ സൃഷ്ടിക്കുക",
        "deadendpages": "അന്തർ വിക്കി കണ്ണിയാൽ ബന്ധിപ്പിക്കപ്പെടാത്ത താളുകൾ",
        "deadendpagestext": "താഴെക്കാണുന്ന താളുകളിൽനിന്ന് {{SITENAME}} സം‌രംഭത്തിലെ മറ്റൊരു താളിലേയ്ക്കും കണ്ണി ചേർത്തിട്ടില്ല.",
        "protectedpages": "സംരക്ഷിക്കപ്പെട്ടിരിക്കുന്ന താളുകൾ",
+       "protectedpages-filters": "അരിപ്പകൾ:",
        "protectedpages-indef": "അനന്തകാലത്തേയ്ക്ക് സംരക്ഷിക്കപ്പെട്ടവ മാത്രം",
        "protectedpages-summary": "ഇപ്പോൾ നിലവിലുള്ള സംരക്ഷിക്കപ്പെട്ടിട്ടുള്ള താളുകളുടെ പട്ടിക ഈ താളിൽ കാണാം. സൃഷ്ടിക്കുന്നതിൽ നിന്നും സംരക്ഷിക്കപ്പെട്ടിട്ടുള്ള തലക്കെട്ടുകൾക്കായി [[{{#special:ProtectedTitles}}|{{int:protectedtitles}}]] കാണുക.",
        "protectedpages-cascade": "നിർഝരിത സംരക്ഷണങ്ങൾ മാത്രം",
        "pagedata-title": "താൾ വിവരങ്ങൾ",
        "pagedata-not-acceptable": "ഒത്തുപോവുന്ന ഫോർമാറ്റ് കണ്ടെത്താനായില്ല. പിന്തുണയുള്ള മൈം തരങ്ങൾ: $1",
        "pagedata-bad-title": "അസാധുവായ തലക്കെട്ട്: $1.",
+       "unregistered-user-config": "സുരക്ഷാകാരണങ്ങളാൽ രജിസ്റ്റർ ചെയ്യാത്ത ഉപയോക്താക്കൾക്ക് ജാവാസ്ക്രിപ്റ്റ്, സി.എസ്.എസ്., ജെസൺ ഉപയോക്തൃ ഉപതാളുകൾ എടുക്കാൻ കഴിയില്ല.",
+       "passwordpolicies": "രഹസ്യവാക്ക് നയങ്ങൾ",
+       "passwordpolicies-summary": "ഉപയോക്തൃ ഗണത്തിനായി ഈ വിക്കിയിൽ നടപ്പിലുള്ള രഹസ്യവാക്ക് നയങ്ങളുടെ പട്ടികയാണ് ഇത്.",
        "passwordpolicies-group": "ഗണം",
        "passwordpolicies-policies": "നയങ്ങൾ",
        "passwordpolicies-policy-minimalpasswordlength": "രഹസ്യവാക്ക് കുറഞ്ഞത് {{PLURAL:$1|ഒരു അക്ഷരം|$1 അക്ഷരങ്ങൾ}} നീളമുള്ളതാവണം.",
        "passwordpolicies-policy-minimumpasswordlengthtologin": "പ്രവേശിക്കാനായി രഹസ്യവാക്ക് കുറഞ്ഞത് {{PLURAL:$1|ഒരു അക്ഷരം|$1 അക്ഷരങ്ങൾ}} നീളമുള്ളതാവണം.",
-       "passwordpolicies-policy-passwordcannotmatchusername": "ഉപയോക്തൃനാമം തന്നെ രഹസ്യവാക്ക് ആകാൻ പാടില്ല"
+       "passwordpolicies-policy-passwordcannotmatchusername": "ഉപയോക്തൃനാമം തന്നെ രഹസ്യവാക്ക് ആകാൻ പാടില്ല",
+       "passwordpolicies-policy-passwordcannotmatchblacklist": "രഹസ്യവാക്ക് കരിമ്പട്ടികയിൽ ഉള്ള രഹസ്യവാക്കുകൾക്ക് സമാനമാകരുത്",
+       "passwordpolicies-policy-maximalpasswordlength": "രഹസ്യവാക്കിന് കുറഞ്ഞത് $1 {{PLURAL:$1|അക്ഷരം|അക്ഷരങ്ങൾ}} നീളമുണ്ടാവണം",
+       "passwordpolicies-policy-passwordcannotbepopular": "രഹസ്യവാക്ക് {{PLURAL:$1|പ്രചുരപ്രചാരത്തിൽ ഉള്ളത്|പ്രചുരപ്രചാരത്തിലുള്ള $1 രഹസ്യവാക്കുകളുടെ പട്ടികയിൽ ഉള്ളത്}} ആകരുത്."
 }
index fb977f9..3dba87c 100644 (file)
        "recentchanges-label-minor": "Chisto è nu cagnamiénto piccerillo",
        "recentchanges-label-bot": "Cagnamiento affettuato 'a nu bot",
        "recentchanges-label-unpatrolled": "Chisto cagno nun è stato 'ncora verificato",
-       "recentchanges-label-plusminus": "'A grannezza d' 'a paggena s'è cagnata pe' bbia 'e stu nummero 'e bytes",
+       "recentchanges-label-plusminus": "Nummero 'e bytes cagnati int' 'a paggena",
        "recentchanges-legend-heading": "<strong>Liggenda:</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (vide [[Special:NewPages|'e paggene nove]])",
        "recentchanges-submit": "Faje vedé",
        "nrevisions": "$1 {{PLURAL:$1|verzione|verziune}}",
        "nimagelinks": "Ausate ncopp'a {{PLURAL:$1|na paggena|$1 paggene}}",
        "ntransclusions": "ausate ncopp'a {{PLURAL:$1|na paggena|$1 paggene}}",
-       "specialpage-empty": "Nun ce stanno risultate pe' stu report.",
+       "specialpage-empty": "Nun ce stanno risultate pe stu report.",
        "lonelypages": "Paggene orfane",
        "lonelypagestext": "'E paggene ccà abbascio nun so state cullegate o appennute int' 'a n'ati paggene ncopp'a {{SITENAME}}.",
        "uncategorizedpages": "Paggene senza categurìa",
        "ipbother": "N'ata durata:",
        "ipboptions": "2 ore:2 hours,1 juorno:1 day,3 juorne:3 days,1 semmana:1 week,2 semmane:2 weeks,1 mese:1 month,3 mise:3 months,6 mise:6 months,1 anno:1 year,infinito:infinite",
        "ipbhidename": "Annascunne 'o nomme utente d' 'a lista 'e cagnamiente e l'ati liste",
-       "ipbwatchuser": "Fà vedé 'a paggena utente e le chiacchieriate 'e st'utente",
+       "ipbwatchuser": "Fà vedé 'a paggena utente e 'e chiacchieriate 'e chist'utente",
        "ipb-disableusertalk": "Nun permettere a st'utente edità 'a paggena 'e chiacchiera d' 'a soja pe' tramente ch'e bloccato",
        "ipb-change-block": "Fremma n'ata vota ll'utente cu ste mpustaziune",
        "ipb-confirm": "Cunferma 'o blocco",
        "version-poweredby-credits": "Sta wiki funziona pe' bbìa 'e <strong>[https://www.mediawiki.org/ MediaWiki]</strong>, copyright © 2001-$1 $2.",
        "version-poweredby-others": "ati",
        "version-poweredby-translators": "tradutture 'e translatewiki.net",
-       "version-credits-summary": "Nuje vulessemo tené a mmente 'e perzune ccà abbascio pe' purtà rispetto a 'e cuntribbute 'e lloro ncopp'a [[Special:Version|MediaWiki]].",
+       "version-credits-summary": "Nuje vulessemo tené a mmente 'e perzune ccà abbascio pe purtà rispetto a 'e cuntribbute 'e lloro ncopp'a [[Special:Version|MediaWiki]].",
        "version-license-info": "MediaWiki è nu software libbero; vuje 'o putite redestribbuì e/o cagnà sott' 'e termine d' 'a licienza GNU GPL ('a Licienza Pubbreca Generale) comme pubbrecata d' 'a Free Software Foundation; o pure 'a verziona 2 d' 'a Licienza, o pure (comme vulite vuje) 'a n'ata verziona cchiù nnova.\n\nMediaWiki è destribbuita c' 'a speranza d'essere utile, ma SENZA NISCIUNA GARANZIA; senza manco 'a garanzia p' 'a CUMMERCIABBELETÀ O IDONIETÀ PE' NU SCOPO PARTICULARE. Iate a vedé 'a GNU GPL pe' n'avé cchiù nfurmaziune.\n\nAvísseve 'a ricevere [{{SERVER}}{{SCRIPTPATH}}/COPYING na copia d' 'a Licienza GNU GPL] cu stu prugramma; si nò, scrivete â Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA o [//www.gnu.org/licenses/old-licenses/gpl-2.0.html liggite sta paggena ncopp' 'a l'Internet].",
        "version-software": "Software installato",
        "version-software-product": "Prodotto",
index 49d4e0c..80d38b6 100644 (file)
        "subject-preview": "Forhåndsvisning av overskrift:",
        "previewerrortext": "En feil oppsto mens dine endringer skulle forhåndsvises.",
        "blockedtitle": "Brukeren er blokkert",
-       "blockedtext": "'''Ditt brukernavn eller din IP-adresse har blitt blokkert.'''\n\nBlokkeringen ble utført av $1. Grunnen som ble oppgitt var ''$2''.\n\n* Blokkeringen begynte: $8\n* Blokkeringen utgår: $6\n* Blokkering ment på: $7\n\nDu kan kontakte $1 eller en annen [[{{MediaWiki:Grouppage-sysop}}|administrator]] for å diskutere blokkeringen.\nDu kan ikke bruke «E-post til denne brukeren»-funksjonen med mindre du har oppgitt en gyldig e-postadresse i [[Special:Preferences|innstillingene dine]] og du ikke er blokkert fra å sende e-post.\nDin nåværende IP-adresse er $3, og blokkerings-ID-en er #$5.\nVennligst ta all denne informasjonen ved henvendelser.",
+       "blockedtext": "<strong>Ditt brukernavn eller din IP-adresse har blitt blokkert.</strong>\n\nBlokkeringen ble utført av $1. Grunnen som ble oppgitt var <em>$2</em>.\n\n* Blokkeringen begynte:  $8\n* Blokkeringen opphører: $6\n* Blokkeringen ment for: $7\n\nDu kan kontakte $1 eller en annen [[{{MediaWiki:Grouppage-sysop}}|administrator]] for å diskutere blokkeringen.\nDu kan ikke bruke \"{{int:emailuser}}\"-funksjonen med mindre du har oppgitt en gyldig e-postadresse i [[Special:Preferences|innstillingene dine]] og du ikke har blitt blokkert fra å sende e-post.\nDin nåværende IP-adresse er $3, og blokkerings-ID-en er #$5.\nVennligst ta med all denne informasjonen ved henvendelser.",
        "autoblockedtext": "Din IP-adresse har blitt automatisk blokkert fordi den ble brukt av en annen bruker som ble blokkert av $1.\nDen oppgitte grunnen var:\n\n:'''$2'''\n\n* Blokkeringen begynte: $8\n* Blokkeringen utgår: $6\n* Blokkeringen er ment for: $7\n\nDu kan kontakte $1 eller en av de andre [[{{MediaWiki:Grouppage-sysop}}|administratorene]] for å diskutere blokkeringen.\n\nMerk at du ikke kan bruke «E-post til denne brukeren»-funksjonen med mindre du har registrert en gyldig e-postadresse i [[Special:Preferences|innstillingene dine]].\n\nDin IP-adresse er $3, og blokkerings-ID-en er #$5.\nVennligst ta med all denne informasjonen ved henvendelser.",
        "systemblockedtext": "Ditt brukernavn eller IP-adresse har blitt blokkert automatisk av MediaWiki.\n\nBlokkeringen grunnes:\n\n:<em>$2</em>\n\n* Blokkeringen startet: $8\n* Blokkeringen gjelder til: $6\n* Blokkeringen er ment for: $7\n\nDin nåværende IP-adresse er $3.\nVennligst inkluder informasjonen over i alle spørsmål du spør angående dette.",
        "blockednoreason": "ingen grunn gitt",
        "rcfilters-watchlist-showupdated": "Endringer til sider du ikke har besøkt siden endringene ble gjort vises med <strong>fet</strong> skrift.",
        "rcfilters-preference-label": "Skjul den forbedrede versjonen av siste endringer",
        "rcfilters-preference-help": "Fjerner grensesnittendringen fra 2017 og alle verktøyene som ble lagt fra og med da.",
+       "rcfilters-watchlist-preference-label": "Skjul den forbedrede versjonen av bevåkningslisten",
+       "rcfilters-watchlist-preference-help": "Ruller tilbake det omarbeidede grensesnittet fra 2017 og alle verktøy som ble lagt til da og etterpå.",
        "rcfilters-filter-showlinkedfrom-label": "Vis endringer på sider som lenkes fra",
        "rcfilters-filter-showlinkedfrom-option-label": "<strong>Sider som lenkes fra</strong> den valgte siden",
        "rcfilters-filter-showlinkedto-label": "Vis endringer på sider som lenker til",
        "apisandbox-dynamic-parameters-add-label": "Legg til parameter:",
        "apisandbox-dynamic-parameters-add-placeholder": "Parameternavn",
        "apisandbox-dynamic-error-exists": "En parameter med navn «$1» finnes fra før.",
+       "apisandbox-templated-parameter-reason": "Denne [[Special:ApiHelp/main#main/templatedparams|templated parameteren]] tilbys basert på {{PLURAL:$1|verdi|verdier}} av $2.",
        "apisandbox-deprecated-parameters": "Utgåtte parametre",
        "apisandbox-fetch-token": "Fyll inn nøkkelen automatisk",
        "apisandbox-add-multi": "Legg til",
        "dellogpage": "Slettelogg",
        "dellogpagetext": "Under er ei liste over nylige slettinger.",
        "deletionlog": "slettelogg",
+       "log-name-create": "Logg over nye sider",
+       "log-description-create": "Under er ei liste over nylige sideopprettelser.",
+       "logentry-create-create": "$1 {{GENDER:$2|opprettet}} side $3",
        "reverted": "Gjenopprettet en tidligere versjon",
        "deletecomment": "Årsak:",
        "deleteotherreason": "Annen/utdypende grunn:",
        "pagedata-text": "Denne siden gir et datagrensesnitt til sidene. Oppgi en sidetittel i URL-en, med undersidesyntaks.\n* Innholdsforhandlingen er basert på din klients Accept-header. Dette betyr at data for siden blir angitt på formatet som foretrekkes av din klient.",
        "pagedata-not-acceptable": "Ingen passende format funnet. Støttede MIME-typer: $1",
        "pagedata-bad-title": "Ugyldig tittel: $1.",
+       "unregistered-user-config": "For å ivareta sikkerhet kan JavaScript, CSS and JSON ikks brukes i underliggende sider for uregistrerte brukere.",
+       "passwordpolicies": "Passord-regler",
+       "passwordpolicies-summary": "Det finnes en liste over gode passord-regler for brukergrupper definert i denne wikien.",
        "passwordpolicies-group": "Gruppe",
+       "passwordpolicies-policies": "Regler",
        "passwordpolicies-policy-minimalpasswordlength": "Passordet må være på minst $1 {{PLURAL:$1|tegn}}.",
        "passwordpolicies-policy-minimumpasswordlengthtologin": "Passordet må være på minst $1 {{PLURAL:$1|tegn}} for å kunne logge inn",
        "passwordpolicies-policy-passwordcannotmatchusername": "Passordet kan ikke være det samme som brukernavnet",
index 11d01f6..45a2f75 100644 (file)
        "viewdeleted_short": "{{PLURAL:$1|Eén versie die vortedaon is|$1 versies die vortedaon bin}} bekieken",
        "protect": "Beveiligen",
        "protect_change": "wiezigen",
-       "unprotect": "Beveyliging wysigen",
+       "unprotect": "Beveiliging wysigen",
        "newpage": "Nieje zied",
        "talkpagelinktext": "Oaverleg",
        "specialpage": "Speciale syde",
index e03fb95..f91ed5b 100644 (file)
        "rcfilters-other-review-tools": "Andere controlehulpmiddelen",
        "rcfilters-group-results-by-page": "Resultaten per pagina groeperen",
        "rcfilters-activefilters": "Actieve filters",
+       "rcfilters-activefilters-hide": "Verbergen",
+       "rcfilters-activefilters-show": "Weergeven",
        "rcfilters-advancedfilters": "Geavanceerde filters",
        "rcfilters-limit-title": "Resultaten om te tonen",
        "rcfilters-limit-and-date-label": "$1 {{PLURAL:$1|wijziging|wijzigingen}}, $2",
        "rcfilters-empty-filter": "Geen actieve filters. Alle bijdragen worden weergegeven.",
        "rcfilters-filterlist-title": "Filters",
        "rcfilters-filterlist-whatsthis": "Hoe werkt dit?",
-       "rcfilters-filterlist-feedbacklink": "Laat ons weten wat u denkt over deze (nieuwe) filterhulpmiddelen",
+       "rcfilters-filterlist-feedbacklink": "Laat ons weten wat u denkt over deze filterhulpmiddelen",
        "rcfilters-highlightbutton-title": "Resultaten markeren",
        "rcfilters-highlightmenu-title": "Kies een kleur",
        "rcfilters-highlightmenu-help": "Selecteer een kleur om deze eigenschap uit te lichten",
index c6225ac..45fe433 100644 (file)
        "resetpass-temp-password": "Tymczasowe hasło:",
        "resetpass-abort-generic": "Zmiana hasła została przerwana przez rozszerzenie.",
        "resetpass-expired": "Twoje hasło wygasło. Proszę ustawić nowe hasło do logowania.",
-       "resetpass-expired-soft": "Twoje hasło wygasło i musi zostać zresetowane. Proszę wybrać nowe hasło albo kliknąć na „{{int:authprovider-resetpass-skip-label}}”, aby zresetować je później.",
-       "resetpass-validity-soft": "Twoje hasło jest niepoprawne: $1\n\nWybierz teraz nowe hasło albo kliknij „{{int:authprovider-resetpass-skip-label}}”, aby zresetować je później.",
+       "resetpass-expired-soft": "Twoje hasło wygasło i musi zostać zmienione. Wybierz nowe hasło albo kliknij na „{{int:authprovider-resetpass-skip-label}}”, aby zmienić je później.",
+       "resetpass-validity-soft": "Twoje hasło jest niepoprawne: $1\n\nWybierz nowe hasło albo kliknij „{{int:authprovider-resetpass-skip-label}}”, aby zmienić je później.",
        "passwordreset": "Wyczyść hasło",
        "passwordreset-text-one": "Wypełnij ten formularz, aby otrzymać tymczasowe hasło na email.",
        "passwordreset-text-many": "{{PLURAL:$1|Wypełnij jedno z poniższych pól, aby otrzymać tymczasowe hasło przez e-mail.}}",
        "rcfilters-other-review-tools": "Inne narzędzia do sprawdzania",
        "rcfilters-group-results-by-page": "Grupuj wyniki według stron",
        "rcfilters-activefilters": "Aktywne filtry",
+       "rcfilters-activefilters-hide": "Ukryj",
+       "rcfilters-activefilters-show": "Pokaż",
        "rcfilters-advancedfilters": "Zaawansowane filtry",
        "rcfilters-limit-title": "Wyników do pokazania",
        "rcfilters-limit-and-date-label": "$1 {{PLURAL:$1|zmiana|zmiany|zmian}}, $2",
        "rcfilters-empty-filter": "Brak aktywnych filtrów. Wyświetlane są wszystkie zmiany.",
        "rcfilters-filterlist-title": "Filtry",
        "rcfilters-filterlist-whatsthis": "Jak to działa?",
-       "rcfilters-filterlist-feedbacklink": "Napisz co sądzisz o tych nowych narzędziach filtrowania",
+       "rcfilters-filterlist-feedbacklink": "Napisz nam, co sądzisz o tych narzędziach filtrowania",
        "rcfilters-highlightbutton-title": "Podświetl wyniki",
        "rcfilters-highlightmenu-title": "Wybierz kolor",
        "rcfilters-highlightmenu-help": "Wybierz kolor, aby podświetlić tę właściwość",
index 3f5a2e7..726c303 100644 (file)
        "page_first": "prima",
        "page_last": "ùltima",
        "histlegend": "Confront antra version diferente: che as selession-a le casele dle version che a veul e peui che a sgnaca ël boton për anandié ël process.<br />\nLegenda: (cor) = diferense con la version corenta,\n(prim) = diferense con la version prima, c = modìfica cita",
-       "history-fieldset-title": "Varda la cronologìa",
+       "history-fieldset-title": "Sërché dle revision",
        "history-show-deleted": "Mach ëscancelà",
        "histfirst": "ij pì vej",
        "histlast": "ij pì recent",
        "searchprofile-advanced-tooltip": "Sërché ant jë spassi nominaj përsonalisà",
        "search-result-size": "$1 ({{PLURAL:$2|un|$2}} mòt)",
        "search-result-category-size": "{{PLURAL:$1|1 mèmber|$1 mèmber}} ({{PLURAL:$2|1 sot-categorìa|$2 sot-categorìe}}, {{PLURAL:$3|1 archivi|$3 archivi}})",
-       "search-redirect": "(ridiression $1)",
+       "search-redirect": "(ridiression da $1)",
        "search-section": "(session $1)",
        "search-category": "(categorìa $1)",
        "search-file-match": "(a corëspond al contnù d'archivi)",
        "recentchangeslinked-feed": "Modìfiche colegà",
        "recentchangeslinked-toolbox": "Modìfiche colegà",
        "recentchangeslinked-title": "Modìfiche ch'a-i intro con \"$1\"",
-       "recentchangeslinked-summary": "Costa a l'é na lista ëd modìfiche fàite da pòch a pàgine colegà a cola spessificà (o a mèmber ëd na categorìa spessificà).\nLe pàgine dzora a [[Special:Watchlist|la lista ëd lòn ch'as ten sot-euj]] a resto marcà an '''grassèt'''.",
+       "recentchangeslinked-summary": "Scrive un nòm ëd na pàgina për vëdde ij cangiament an s'ëd pàgine colegà vers o da cola pàgina. (Për vëdde ij mèmber ëd na categorìa, ch'a scriva {{ns:category}}:Nòm ëd categorìa). Ij cangiament a dle pàgine dla [[Special:Watchlist|ròba ch'as ten sot-euj]] a son an <strong>grassèt</strong>.",
        "recentchangeslinked-page": "Nòm ëd la pàgina:",
        "recentchangeslinked-to": "Mostré nopà le modìfiche a le pàgine colegà a cola dàita",
        "upload": "Carié n'archivi",
        "booksources-text": "Ambelessì sota a-i é na lista d'àotri sit che a vendo dij lìber neuv e dë sconda man, e che a peulo ëdcò smon-e dj'anformassion rësgoard ai test che a l'é antramentr che a sërca:",
        "booksources-invalid-isbn": "L'ISBN dàit a smija che a sia pa vàlid; ch'a contròla s'a-i é n'eror an copiand da la sorgiss original.",
        "specialloguserlabel": "Esecutor:",
-       "speciallogtitlelabel": "Obietiv (tìtol o utent):",
+       "speciallogtitlelabel": "Obietiv (tìtol o {{ns:user}}:stranòm ëd l'utent):",
        "log": "Registr",
        "all-logs-page": "Tùit ij registr pùblich",
        "alllogstext": "Visualisassion combinà ëd tùit ij registr ëd {{SITENAME}}.\nA peul arstrenze la visualisassion an selessionand la sòrt ëd registr, lë stranòm utent (sensìbil a majùscol/minùscol), e la pàgina anteressà (sensìbil a majùscol/minùscol).",
        "unwatchthispage": "Chité-lì ëd ten-e sossì sot-euj",
        "notanarticle": "Sòn a l'é pa n'artìcol",
        "notvisiblerev": "La revision a l'é stàita scancelà",
-       "watchlist-details": "A l'é dëmentrè ch'as ten sot-euj {{PLURAL:$1|$1 pàgina|$1 pàgine}}, nen contand an manera separà cole ëd discussion.",
+       "watchlist-details": "La lista 'd lòn ch'as ten sot-euj {{PLURAL:$1|$1 pàgina|$1 pàgine}}, nen contand an manera separà cole ëd discussion.",
        "wlheader-enotif": "La notìfica për pòsta eletrònica a l'é abilità.",
        "wlheader-showupdated": "Le pàgine che a son ëstàite modificà da quand che a l'é passaje ansima l'ùltima vira a resto marcà an '''grassèt'''",
        "wlnote": "Ambelessì sota a-i {{PLURAL:$1|é l'ùltima modìfica|son j'ùltime <strong>$1</strong> modìfiche}} ant {{PLURAL:$2|l'ùltima ora|j'ùltime <strong>$2</strong> ore}}, a parte da $3, $4.",
        "whatlinkshere-hideredirs": "$1 ridiression",
        "whatlinkshere-hidetrans": "$1 anclusion",
        "whatlinkshere-hidelinks": "$1 anliure",
-       "whatlinkshere-hideimages": "$1 j'archivi lijà",
+       "whatlinkshere-hideimages": "$1 liure da archivi",
        "whatlinkshere-filters": "Filtr",
        "autoblockid": "Blocagi automàtich #$1",
        "block": "Bloché l'utent",
        "tooltip-feed-rss": "Fluss RSS për costa pàgina",
        "tooltip-feed-atom": "Fluss Atom për costa pàgina.",
        "tooltip-t-contributions": "Vardé la lista dle contribussion ëd {{GENDER:$1|cost utent}}",
-       "tooltip-t-emailuser": "Mandeje un mëssagi ëd pòsta a st'utent",
+       "tooltip-t-emailuser": "Mandé 'n mëssaggi ëd pòsta eletrònica a {{GENDER:$1|cost|cost}} utent",
        "tooltip-t-info": "Pi d'anformassio su costa pàgina",
        "tooltip-t-upload": "Carié n'archivi ëd figure ò son.",
        "tooltip-t-specialpages": "Lista ëd tute le pàgine speciaj.",
        "version-libraries": "Biblioteche anstalà",
        "version-libraries-library": "Biblioteca",
        "version-libraries-version": "Version",
-       "redirect": "Ridirigiù da archivi, utent, pàgina o ID ëd revision",
+       "redirect": "Ridiression për ID d'archivi, utent, pàgina, revision o argistr",
        "redirect-summary": "Costa pàgina special a ponta a n'archivi (dàit ël nòm dl'archivi), na pàgina (dàit n'ID ëd revision o n'ID ëd pàgina) o na pàgina d'utent (dàit n'identificativ numérich a l'utent). Usagi: [[{{#Special:Redirect}}/file/Example.jpg]], [[{{#Special:Redirect}}/revision/328429]], o [[{{#Special:Redirect}}/user/101]].",
        "redirect-submit": "Andé",
        "redirect-lookup": "Arserca:",
        "feedback-thanks": "Mersì! Sò coment a l'é stàit publicà an sla pàgina \"[$2 $1]\".",
        "feedback-thanks-title": "Mersì!",
        "feedback-useragent": "Agent d'utent:",
-       "searchsuggest-search": "Arserché",
+       "searchsuggest-search": "Sërca an {{SITENAME}}",
        "searchsuggest-containing": "contenent ...",
        "api-error-badtoken": "Eror antern: sìmbol pa bon.",
        "api-error-emptypage": "La creassion ëd pàgine neuve veujde a l'é nen përmëttùa.",
index ca356b6..a77ff2d 100644 (file)
        "resetpass-submit-loggedin": "Alterar senha",
        "resetpass-submit-cancel": "Cancelar",
        "resetpass-wrong-oldpass": "Senha temporária ou atual inválida.\nVocê pode já ter alterado com sucesso a sua senha, ou solicitado uma nova senha temporária.",
-       "resetpass-recycled": "Por favor, redefina sua nova senha para uma diferente da atual.",
+       "resetpass-recycled": "Por favor, altere sua senha para algo diferente da sua senha atual.",
        "resetpass-temp-emailed": "Você está autenticado com o código temporário enviado. Para finalizar a autenticação, você deve inserir uma nova senha aqui:",
        "resetpass-temp-password": "Senha temporária:",
        "resetpass-abort-generic": "Uma extensão cancelou a alteração da senha.",
        "resetpass-expired": "Sua senha expirou. Por favor insira uma nova senha para autenticar-se.",
-       "resetpass-expired-soft": "Sua senha expirou e necessita ser resetada. Por favor escolha uma nova agora, ou clique \"{{int:authprovider-resetpass-skip-label}}\" para resetar mais tarde.",
-       "resetpass-validity-soft": "Sua senha não é válida: $1\n\nPor favor escolha uma nova senha agora, ou clique \"{{int:authprovider-resetpass-skip-label}}\" para redefini-la mais tarde.",
+       "resetpass-expired-soft": "Sua senha expirou e precisa ser alterada. Escolha uma nova senha agora ou clique em \"{{int:authprovider-resetpass-skip-label}}\" para alterá-la mais tarde.",
+       "resetpass-validity-soft": "Sua senha não é válida: $1\n\nPor favor escolha uma nova senha agora, ou clique \"{{int:authprovider-resetpass-skip-label}}\" para mudar depois.",
        "passwordreset": "Redefinir senha",
        "passwordreset-text-one": "Complete este formulário para trocar sua senha.",
        "passwordreset-text-many": "{{PLURAL:$1|Preencha um dos campos para trocar sua senha.}}",
        "prefs-help-gender": "A configuração dessa preferência é opcional.\nO ''software'' utiliza seu valor para tratar e mencionar você a outros usando o gênero gramatical adequado.\nEssa informação será pública.",
        "email": "E-mail",
        "prefs-help-realname": "O fornecimento de seu nome verdadeiro é opcional.\nCaso decida fornecê-lo, este será utilizado para dar-lhe crédito pelo seu trabalho.",
-       "prefs-help-email": "O endereço de correio eletrônico é opcional, mas será necessário para recriar sua senha caso esqueça a antiga.",
+       "prefs-help-email": "O endereço de e-mail é opcional, mas será necessário para recriar sua senha caso esqueça a antiga.",
        "prefs-help-email-others": "Você também pode optar por permitir que outros entrem em contato com você através de sua página de usuário ou de discussão sem ter de revelar seus dados pessoais.",
        "prefs-help-email-required": "O endereço de e-mail é requerido.",
        "prefs-info": "Informações básicas",
        "rcfilters-other-review-tools": " Outras ferramentas de revisão:",
        "rcfilters-group-results-by-page": "Agrupar resultado por página",
        "rcfilters-activefilters": "Filtros ativos",
+       "rcfilters-activefilters-hide": "Ocultar",
+       "rcfilters-activefilters-show": "Exibir",
        "rcfilters-advancedfilters": "Filtros avançados",
        "rcfilters-limit-title": "Resultados para mostrar",
        "rcfilters-limit-and-date-label": "$1 {{PLURAL:$1|mudança|mudanças}}, $2",
        "rcfilters-savedqueries-rename": "Renomear",
        "rcfilters-savedqueries-setdefault": "Definir como padrão",
        "rcfilters-savedqueries-unsetdefault": "Remover como padrão",
-       "rcfilters-savedqueries-remove": "Remover",
+       "rcfilters-savedqueries-remove": "Excluir",
        "rcfilters-savedqueries-new-name-label": "Nome",
        "rcfilters-savedqueries-new-name-placeholder": "Descreva a finalidade do filtro",
        "rcfilters-savedqueries-apply-label": "Criar filtro",
        "rcfilters-empty-filter": "Nenhum filtro ativo. Todas as contribuições são mostradas.",
        "rcfilters-filterlist-title": "Filtros",
        "rcfilters-filterlist-whatsthis": "Como funcionam estes?",
-       "rcfilters-filterlist-feedbacklink": "Diga-nos o que você pensa sobre estas (novas) ferramentas de filtragem",
+       "rcfilters-filterlist-feedbacklink": "Conte-nos o que você pensa sobre essas ferramentas de filtragem",
        "rcfilters-highlightbutton-title": "Destacar resultados",
        "rcfilters-highlightmenu-title": "Selecione uma cor",
        "rcfilters-highlightmenu-help": "Selecione uma cor para realçar esta propriedade",
        "rcfilters-watchlist-showupdated": "As alterações nas páginas que você não visitou desde as mudanças ocorridas estão em <strong>negrito</strong>, com marcadores sólidos.",
        "rcfilters-preference-label": "Ocultar a versão melhorada das Mudanças Recentes",
        "rcfilters-preference-help": "Reverte o redesenho da interface de 2017 e todas as ferramentas adicionadas na altura e desde então.",
+       "rcfilters-watchlist-preference-label": "Ocultar a versão melhorada das páginas vigiadas",
+       "rcfilters-watchlist-preference-help": "Reverte o redesenho da interface em 2017 e todas as ferramentas adicionadas nessa altura e desde então.",
        "rcfilters-filter-showlinkedfrom-label": "Mostrar alterações nas páginas ligadas de",
        "rcfilters-filter-showlinkedfrom-option-label": "<strong>Páginas ligadas da</strong> página selecionada",
        "rcfilters-filter-showlinkedto-label": "Mostrar alterações nas páginas que ligam para",
        "dellogpage": "Registro de eliminações",
        "dellogpagetext": "Abaixo uma lista das eliminações mais recentes.",
        "deletionlog": "registro de eliminações",
+       "log-name-create": "Registo de criação de páginas",
+       "log-description-create": "Encontra abaixo uma lista das criações de páginas mais recentes.",
+       "logentry-create-create": "$1 {{GENDER:$2|criou}} a página $3",
        "reverted": "Revertido para versão anterior",
        "deletecomment": "Motivo:",
        "deleteotherreason": "Justificativa adicional:",
        "sp-contributions-newbies-title": "Contribuições de contas novas",
        "sp-contributions-blocklog": "registro de bloqueios",
        "sp-contributions-suppresslog": "Contribuições de {{GENDER:$1|usuário}} suprimidas",
-       "sp-contributions-deleted": "contribuições eliminadas",
+       "sp-contributions-deleted": "{{GENDER:$1|contribuições}} eliminadas",
        "sp-contributions-uploads": "envios",
        "sp-contributions-logs": "registros",
        "sp-contributions-talk": "discussão",
-       "sp-contributions-userrights": "gerenciamento de privilégios",
+       "sp-contributions-userrights": "{{GENDER:$1|gestão}} dos privilégios",
        "sp-contributions-blocked-notice": "Este usuário atualmente está bloqueado. O registro de bloqueio mais recente é fornecido abaixo para referência:",
        "sp-contributions-blocked-notice-anon": "Este endereço IP encontra-se bloqueado.\nSegue, para referência, a entrada mais recente no registro de bloqueios:",
        "sp-contributions-search": "Navegar pelas contribuições",
        "rawhtml-notallowed": "As tags &lt;html&gt; não podem ser usadas fora das páginas normais.",
        "gotointerwiki": "Saindo {{SITENAME}}",
        "gotointerwiki-invalid": "O título especificado é inválido.",
-       "gotointerwiki-external": "Você está prestes a sair {{SITENAME}} para visitar [[$2]] que é um website separado.\n\n[$1 Continuar para $1].",
+       "gotointerwiki-external": "Você está prestes a sair {{SITENAME}} para visitar [[$2]] que é um website separado.\n\n'''[$1 Continuar para $1]'''",
        "undelete-cantedit": "Você não pode restaurar esta página, porque você não está autorizado a editar esta página.",
        "undelete-cantcreate": "Você não pode restaurar esta página, pois não há uma página com este nome e você não está autorizado a criar esta página.",
        "pagedata-title": "Dados de página",
index cb216ce..eed76f1 100644 (file)
        "resetpass-submit-loggedin": "Alterar palavra-passe",
        "resetpass-submit-cancel": "Cancelar",
        "resetpass-wrong-oldpass": "Palavra-passe temporária ou atual inválida.\nPode ter já alterado a sua palavra-passe ou solicitado uma nova palavra-passe temporária.",
-       "resetpass-recycled": "Redefina a sua palavra-passe para uma diferente da atual, por favor.",
+       "resetpass-recycled": "Altere a sua palavra-passe para uma diferente da atual, por favor.",
        "resetpass-temp-emailed": "Iniciou a sessão com um código temporário.\nPara completar a autenticação, tem de definir uma palavra-passe nova aqui:",
        "resetpass-temp-password": "Palavra-passe temporária:",
        "resetpass-abort-generic": "A alteração da palavra-passe foi cancelada por uma extensão.",
        "resetpass-expired": "A sua palavra-passe expirou. Por favor, defina uma nova para iniciar a sessão.",
-       "resetpass-expired-soft": "A sua palavra-passe expirou e tem de ser redefinida. Escolha uma nova agora ou clique \"{{int:authprovider-resetpass-skip-label}}\" para redefini-la mais tarde.",
-       "resetpass-validity-soft": "A sua palavra-passe não é válida: $1\n\nPor favor, escolha uma nova palavra-passe agora, ou clique em \"{{int:authprovider-resetpass-skip-label}}\" para redefini-la mais tarde.",
+       "resetpass-expired-soft": "A sua palavra-passe expirou e tem de ser alterada. Escolha uma nova agora ou clique \"{{int:authprovider-resetpass-skip-label}}\" para alterá-la mais tarde.",
+       "resetpass-validity-soft": "A sua palavra-passe não é válida: $1\n\nEscolha uma palavra-passe nova agora, ou clique em \"{{int:authprovider-resetpass-skip-label}}\" para alterá-la mais tarde, por favor.",
        "passwordreset": "Redefinir palavra-passe",
        "passwordreset-text-one": "Preencha este formulário para receber uma palavra-passe temporária por correio eletrónico.",
        "passwordreset-text-many": "{{PLURAL:$1|Preencha um dos campos para receber uma palavra-passe temporária por correio eletrónico.}}",
        "rcfilters-other-review-tools": "Outras ferramentas de revisão",
        "rcfilters-group-results-by-page": "Agrupar resultados por página",
        "rcfilters-activefilters": "Filtros ativos",
+       "rcfilters-activefilters-hide": "Ocultar",
+       "rcfilters-activefilters-show": "Mostrar",
        "rcfilters-advancedfilters": "Filtros avançados",
        "rcfilters-limit-title": "Resultados a mostrar",
        "rcfilters-limit-and-date-label": "$1 {{PLURAL:$1|mudança|mudanças}}, $2",
        "rcfilters-savedqueries-rename": "Alterar o nome",
        "rcfilters-savedqueries-setdefault": "Ativar por padrão",
        "rcfilters-savedqueries-unsetdefault": "Remover por padrão",
-       "rcfilters-savedqueries-remove": "Remover",
+       "rcfilters-savedqueries-remove": "Eliminar",
        "rcfilters-savedqueries-new-name-label": "Nome",
        "rcfilters-savedqueries-new-name-placeholder": "Descreve o propósito do filtro",
        "rcfilters-savedqueries-apply-label": "Criar filtro",
        "rcfilters-empty-filter": "Não há filtros ativos. São mostradas todas as contribuições.",
        "rcfilters-filterlist-title": "Filtros",
        "rcfilters-filterlist-whatsthis": "Como é que funcionam?",
-       "rcfilters-filterlist-feedbacklink": "Diga-nos o que acha das (novas) ferramentas de filtragem",
+       "rcfilters-filterlist-feedbacklink": "Diga-nos o que acha destas ferramentas de filtragem",
        "rcfilters-highlightbutton-title": "Realçar resultados",
        "rcfilters-highlightmenu-title": "Selecionar uma cor",
        "rcfilters-highlightmenu-help": "Selecione uma cor para realçar esta propriedade",
index f703862..0b92009 100644 (file)
        "rcfilters-other-review-tools": "Used as a heading for the community collection of other links on [[Special:RecentChanges]] when RCFilters are enabled.",
        "rcfilters-group-results-by-page": "A label for the checkbox describing whether the results in [[Special:RecentChanges]] are grouped by page when RCFilters are enabled.",
        "rcfilters-activefilters": "{{doc-important|Translations of this message should not more than 3 cm long, otherwise it will make bad user experiences for potential mobile users.}}\nTitle for the filters selection showing the active filters.",
+       "rcfilters-activefilters-hide": "Label for the button that hides the active filters list and dropdown in [[Special:RecentChanges]].\n{{Identical|Hide}}",
+       "rcfilters-activefilters-show": "Label for the button that shows the active filters list and dropdown in [[Special:RecentChanges]].\n{{Identical|Show}}",
        "rcfilters-advancedfilters": "Title for the buttons allowing the user to switch to the various advanced filters views.",
        "rcfilters-limit-title": "Title for the options to change the number of results shown.",
        "rcfilters-limit-and-date-label": "Title for the button that opens the operation to control how many results to show and in which time period to search. \n\nParameters: $1 - Number of results shown\n\n$2 - Time period to search. One of {{msg-mw|rcfilters-days-title}} or {{msg-mw|rcfilters-hours-title}} is used as $2\n{{Identical|Change}}",
        "rcfilters-savedqueries-rename": "Label for the menu option that edits a quick filter in [[Special:RecentChanges]]\n{{Identical|Rename}}",
        "rcfilters-savedqueries-setdefault": "Label for the menu option that sets a quick filter as default in [[Special:RecentChanges]]",
        "rcfilters-savedqueries-unsetdefault": "Label for the menu option that unsets a quick filter as default in [[Special:RecentChanges]]",
-       "rcfilters-savedqueries-remove": "Label for the menu option that removes a quick filter as default in [[Special:RecentChanges]]\n{{Identical|Remove}}",
+       "rcfilters-savedqueries-remove": "Label for the menu option that removes a quick filter as default in [[Special:RecentChanges]]\n{{Identical|Delete}}",
        "rcfilters-savedqueries-new-name-label": "Label for the input that holds the name of the new saved filters in [[Special:RecentChanges]]\n{{Identical|Name}}",
        "rcfilters-savedqueries-new-name-placeholder": "Placeholder for the input that holds the name of the new saved filters in [[Special:RecentChanges]]",
        "rcfilters-savedqueries-apply-label": "Label for the button to apply saving a new filter setting in [[Special:RecentChanges]]. This is for a small popup, please try to use a short string.",
index 2b86286..5994991 100644 (file)
        "resetpass-abort-generic": "Изменение пароля было прервано расширением.",
        "resetpass-expired": "Срок действия вашего пароля истёк. Пожалуйста, установите новый пароль для входа в систему.",
        "resetpass-expired-soft": "Срок действия вашего пароля истёк, и теперь он должен быть изменён. Пожалуйста, выберите новый пароль или нажмите «{{int:authprovider-resetpass-skip-label}}», чтобы изменить его позже.",
-       "resetpass-validity-soft": "Задан некорректный пароль: $1\n\nПожалуйста, выберите новый пароль или нажмите «{{int:authprovider-resetpass-skip-label}}», чтобы сбросить его позже.",
+       "resetpass-validity-soft": "Задан некорректный пароль: $1\n\nПожалуйста, выберите новый пароль или нажмите «{{int:authprovider-resetpass-skip-label}}», чтобы изменить его позже.",
        "passwordreset": "Сброс пароля",
        "passwordreset-text-one": "Заполните эту форму, чтобы сбросить свой пароль.",
        "passwordreset-text-many": "{{PLURAL:$1|Заполните одно из полей для получения временного пароля по электронной почте.}}",
        "rcfilters-other-review-tools": "Другие инструменты проверки",
        "rcfilters-group-results-by-page": "Группировать результаты по странице",
        "rcfilters-activefilters": "Активные фильтры",
+       "rcfilters-activefilters-hide": "Скрыть",
+       "rcfilters-activefilters-show": "Показать",
        "rcfilters-advancedfilters": "Расширенные фильтры",
        "rcfilters-limit-title": "Результаты для показа",
        "rcfilters-limit-and-date-label": "$1 {{PLURAL:$1|изменение|изменения|изменений}}, $2",
        "rcfilters-empty-filter": "Нет активных фильтров. Показываются все правки.",
        "rcfilters-filterlist-title": "Фильтры",
        "rcfilters-filterlist-whatsthis": "Как это работает?",
-       "rcfilters-filterlist-feedbacklink": "Расскажите нам, что вы думаете об этих (новых) инструментах фильтрации",
+       "rcfilters-filterlist-feedbacklink": "Расскажите нам, что вы думаете об этих инструментах фильтрации",
        "rcfilters-highlightbutton-title": "Выделить результаты",
        "rcfilters-highlightmenu-title": "Выберите цвет",
        "rcfilters-highlightmenu-help": "Выберите цвет, чтобы подсветить это свойство",
index 3017efb..5dae666 100644 (file)
@@ -9,7 +9,8 @@
                        "Macofe",
                        "Indus Asia",
                        "BukhariSaeed",
-                       "Fitoschido"
+                       "Fitoschido",
+                       "Tweety"
                ]
        },
        "tog-underline": "ڳنڍڻي هيٺان لڪير:",
        "summary-preview": "تت جي پيش نگاھ:",
        "subject-preview": "موضوع جي پيش نگاھ:",
        "blockedtitle": "واپرائيندڙ بندشيل آهي",
-       "blockedtext": "'''توھان جي واپرائيندڙ-نانءُ يا آءِ پي کي بندشيو ويو آھي.'''\n\nبندش $1 ھنئي. جڏھن تہ ڄاڻايل سبب ''$2'' آهي.\n\n\n* بندش جو آغاز: $8\n* بندش جو انجام: $6\n* بندش جو هدف: $7\n\nاھڙي روڪ تي بحث ڪرڻ لاءِ توھان $1 يا ڪنھن ٻي [[{{MediaWiki:Grouppage-sysop}}|منتظم]] سان رابطو ڪري سگھو ٿا. جيڪڏهن توھان جو درست [[Special:Preferences|کاتي جي ترجيحن]] ۾ درست برقٽپال پتو درج ٿيل نہ آهي تہ توهان 'هن واپرائيندڙ کي برقٽپال ڪريو' وارو فيچر نہ ٿا \nاستعمال ڪري سگھو. توھان جو ھاڻوڪو آءِپي پتو $3 آھي، ۽ بندش سڃاڻپ $5 آهي. مھرباني ڪري ڪنھن بہ پڇا ڳاڇا يا لھ و چڙھ لاءِ انھن مان ڪنھن ھڪ يا ٻنھي جو حوالو ڏيندا.",
+       "blockedtext": "<strong>توھان جي واپرائيندڙ-نانءُ يا آءِ پي کي بندشيو ويو آھي.</strong>\n\nبندش $1 ھنئي. جڏھن تہ ڄاڻايل سبب ''$2'' آهي.\n\n\n* بندش جو آغاز: $8\n* بندش جو انجام: $6\n* بندش جو هدف: $7\n\nاھڙي روڪ تي بحث ڪرڻ لاءِ توھان $1 يا ڪنھن ٻي [[{{MediaWiki:Grouppage-sysop}}|منتظم]] سان رابطو ڪري سگھو ٿا. جيڪڏهن توھان جو درست [[Special:Preferences|کاتي جي ترجيحن]] ۾ درست برقٽپال پتو درج ٿيل نہ آهي تہ توهان 'هن واپرائيندڙ کي برقٽپال ڪريو' وارو فيچر نہ ٿا \nاستعمال ڪري سگھو. توھان جو ھاڻوڪو آءِپي پتو $3 آھي، ۽ بندش سڃاڻپ $5 آهي. مھرباني ڪري ڪنھن بہ پڇا ڳاڇا يا لھ و چڙھ لاءِ انھن مان ڪنھن ھڪ يا ٻنھي جو حوالو ڏيندا.",
        "blockednoreason": "سبب اڻڄاڻايل",
        "whitelistedittext": "صفحا سنوارڻ لاءِ مھرباني ڪري $1.",
        "confirmedittext": "صفحا سنوارڻ کان اڳ توھان کي پنھنجي برقٽپال پتي جي تصديق ڪرڻي پوندي.\nمھرباني ڪري [[Special:Preferences|واپرائيندڙ جي ترجيحن]] ذريعي پنھنجو برقٽپال پتو ڄاڻايو ۽ تصديقيو.",
        "recentchangeslinked-feed": "لاڳاپيل تبديليون",
        "recentchangeslinked-toolbox": "لاڳاپيل تبديليون",
        "recentchangeslinked-title": "\"$1\" سان لاڳاپيل تبديليون",
+       "recentchangeslinked-summary": "تبديليون ڏسڻ لاءِ صفحي جو نالو هڻو پوءِ اها هن صفحي تي هجن يا ڳنڍيل صفحي تي. (زمري جارُڪن ڏسڻ لاءِ، {{ns:زمرو}}:زمري جو نالو)هڻو. [[Special:Watchlist|your Watchlist]] صفحي تي تبديليون <strong>bold</strong> ۾ آهن.",
        "recentchangeslinked-page": "صفحي جو نالو:",
        "recentchangeslinked-to": "رڳو ڄاڻايل صفحي سان ڳانڍيل صفحن ۾ ٿيل تبديليون نمايو",
        "upload": "فائيل چاڙھيو",
        "anoncontribs": "ڀاڱيداريون",
        "contribsub2": "{{GENDER:$3|$1}} ($2) لاءِ",
        "contributions-userdoesnotexist": "واپرائيندڙ کاتو \"$1\" درج ٿيل نہ آهي.",
+       "nocontribs": "هن معيار تي ڪي به ترميمون نه لڌيون.",
        "uctop": "(هاڻوڪو)",
        "month": "مھيني کان (۽ اڳوڻيون):",
        "year": "سال کان (۽ اڳوڻيون):",
        "pageinfo-language": "صفحي جي مواد جي ٻولي",
        "pageinfo-content-model": "صفحي جي مواد جو ماڊل",
        "pageinfo-robot-index": "اجازت ڏنل",
+       "pageinfo-robot-noindex": "اجازت ناهي",
        "pageinfo-watchers": "صفحا ڏسندڙن جو انگ",
        "pageinfo-few-watchers": "$1 کان گھٽ {{PLURAL:$1|ڏسندڙ}}",
        "pageinfo-redirects-name": "ھن صفحي ڏانھن ڇوريل صفحن جو انگ",
index 82a500a..69a38b4 100644 (file)
        "savechanges": "Uložiť zmeny",
        "publishpage": "Publikovať stránku",
        "publishchanges": "Publikovať zmeny",
+       "publishchanges-start": "Zverejniť zmeny…",
        "preview": "Náhľad",
        "showpreview": "Zobraziť náhľad",
        "showdiff": "Zobraziť rozdiely",
index 57fa71e..90ec173 100644 (file)
        "resetpass-submit-loggedin": "Spremenite geslo",
        "resetpass-submit-cancel": "Prekliči",
        "resetpass-wrong-oldpass": "Neveljavno začasno ali trenutno geslo.\nMorda ste že spremenili geslo ali pa ste zahtevali novo začasno geslo.",
-       "resetpass-recycled": "Prosimo, ponastavite svoje geslo na nekaj drugega kot svoje trenutno geslo.",
+       "resetpass-recycled": "Prosimo, spremenite svoje geslo na nekaj drugega kot svoje trenutno geslo.",
        "resetpass-temp-emailed": "Trenutno ste prijavljeni z začasno kodo, poslano z e-pošto. Za zaključitev prijave morate tukaj nastaviti novo geslo:",
        "resetpass-temp-password": "Začasno geslo:",
        "resetpass-abort-generic": "Razširitev je prekinila spremembo gesla.",
        "resetpass-expired": "Vaše geslo je poteklo. Prosimo, nastavite novo geslo za prijavo.",
-       "resetpass-expired-soft": "Vaše geslo je poteklo in ga morate ponastaviti. Prosimo, izberite novo geslo zdaj ali kliknite »{{int:authprovider-resetpass-skip-label}}«, da ga ponastavite pozneje.",
-       "resetpass-validity-soft": "Vaše geslo ni veljavno: $1\n\nProsimo, izberite novo geslo zdaj ali kliknite »{{int:authprovider-resetpass-skip-label}}«, da ga ponastavite pozneje.",
+       "resetpass-expired-soft": "Vaše geslo je poteklo in ga morate spremeniti. Prosimo, izberite novo geslo zdaj ali kliknite »{{int:authprovider-resetpass-skip-label}}«, da ga spremenite pozneje.",
+       "resetpass-validity-soft": "Vaše geslo ni veljavno: $1\n\nProsimo, izberite novo geslo zdaj ali kliknite »{{int:authprovider-resetpass-skip-label}}«, da ga spremenite pozneje.",
        "passwordreset": "Ponastavitev gesla",
        "passwordreset-text-one": "Da ponastavite svoje geslo, izpolnite naslednji obrazec.",
        "passwordreset-text-many": "{{PLURAL:$1|Da po e-pošti prejmete začasno geslo, izpolnite eno od polj.}}",
        "rcfilters-other-review-tools": "Druga orodja za pregled",
        "rcfilters-group-results-by-page": "Združi rezultate po strani",
        "rcfilters-activefilters": "Dejavni filtri",
+       "rcfilters-activefilters-hide": "Skrij",
+       "rcfilters-activefilters-show": "Prikaži",
        "rcfilters-advancedfilters": "Napredni filtri",
        "rcfilters-limit-title": "Rezultati za prikaz",
        "rcfilters-limit-and-date-label": "$1 {{PLURAL:$1|sprememba|spremembi|spremembe|sprememb}}, $2",
        "rcfilters-savedqueries-rename": "Preimenuj",
        "rcfilters-savedqueries-setdefault": "Nastavi kot privzeto",
        "rcfilters-savedqueries-unsetdefault": "Odstrani kot privzeto",
-       "rcfilters-savedqueries-remove": "Odstrani",
+       "rcfilters-savedqueries-remove": "Izbriši",
        "rcfilters-savedqueries-new-name-label": "Ime",
        "rcfilters-savedqueries-new-name-placeholder": "Opišite namen filtra",
        "rcfilters-savedqueries-apply-label": "Ustvari filter",
        "rcfilters-empty-filter": "Ni dejavnih filtrov. Prikazani so vsi prispevki.",
        "rcfilters-filterlist-title": "Filtri",
        "rcfilters-filterlist-whatsthis": "Kako to deluje?",
-       "rcfilters-filterlist-feedbacklink": "Povejte nam, kaj menite o teh (novih) orodjih za filtriranje",
+       "rcfilters-filterlist-feedbacklink": "Povejte nam, kaj menite o teh orodjih za filtriranje",
        "rcfilters-highlightbutton-title": "Označi rezultate",
        "rcfilters-highlightmenu-title": "Izberite barvo",
        "rcfilters-highlightmenu-help": "Izberite barvo za označitev te lastnosti",
        "statistics": "Statistika",
        "statistics-header-pages": "Statistika strani",
        "statistics-header-edits": "Statistika urejanj",
-       "statistics-header-users": "Uporabniška statistika",
+       "statistics-header-users": "Statistika uporabnikov",
        "statistics-header-hooks": "Drugi statistični podatki",
        "statistics-articles": "Članki",
        "statistics-pages": "Strani",
index f34bbc3..32944c2 100644 (file)
        "subject-preview": "Преглед теме:",
        "previewerrortext": "Догодила се грешка приликом приказивања ваших измена.",
        "blockedtitle": "Корисник је блокиран",
-       "blockedtext": "<strong>Ð\92аÑ\88е ÐºÐ¾Ñ\80иÑ\81ниÑ\87ко Ð¸Ð¼Ðµ Ð¸Ð»Ð¸ IP Ð°Ð´Ñ\80еÑ\81а Ñ\98е Ð±Ð»Ð¾ÐºÐ¸Ñ\80ана.</strong>\n\nÐ\91локиÑ\80аÑ\9aе Ñ\98е {{GENDER:$4|извÑ\80Ñ\88ио|извÑ\80Ñ\88ила}} $1.\nРазлог Ñ\98е <em>$2</em>.\n\n* Ð\94аÑ\82Ñ\83м Ð±Ð»Ð¾ÐºÐ¸Ñ\80аÑ\9aа: $8\n* Ð\91локиÑ\80аÑ\9aе Ð¸Ñ\81Ñ\82иÑ\87е: $6\n* Ð\98ме ÐºÐ¾Ñ\80иÑ\81ника: $7\n\nÐ\9eбÑ\80аÑ\82иÑ\82е Ñ\81е {{GENDER:$4|коÑ\80иÑ\81никÑ\83|коÑ\80иÑ\81ниÑ\86и}} $1 Ð¸Ð»Ð¸ [[{{MediaWiki:Grouppage-sysop}}|админиÑ\81Ñ\82Ñ\80аÑ\82оÑ\80Ñ\83]] Ð´Ð° Ñ\80азÑ\98аÑ\81ниÑ\82е Ñ\81Ñ\82ваÑ\80.\nÐ\9dе Ð¼Ð¾Ð¶ÐµÑ\82е ÐºÐ¾Ñ\80иÑ\81Ñ\82иÑ\82и Ð¼Ð¾Ð³Ñ\83Ñ\9bноÑ\81Ñ\82 â\80\9eÐ\9fоÑ\88аÑ\99и Ð¸Ð¼ÐµÑ\98л Ð¾Ð²Ð¾Ð¼ ÐºÐ¾Ñ\80иÑ\81никÑ\83â\80\9c Ð°ÐºÐ¾ Ð½Ð¸Ñ\81Ñ\82е Ñ\83нели Ð¸Ñ\81пÑ\80авнÑ\83 Ð¸Ð¼ÐµÑ\98л Ð°Ð´Ñ\80еÑ\81Ñ\83 Ñ\83 [[Special:Preferences|подеÑ\88аваÑ\9aима]].\nÐ\92аÑ\88а Ð±Ð»Ð¾ÐºÐ¸Ñ\80ана IP Ð°Ð´Ñ\80еÑ\81а Ñ\98е $3, Ð° ID Ð±Ð»Ð¾ÐºÐ¸Ñ\80аÑ\9aа $5.\nÐ\9dаведиÑ\82е Ñ\81ве Ð¿Ð¾Ð´Ð°Ñ\82ке Ð¸Ð·Ð½Ð°Ð´ при стварању било каквих упита.",
+       "blockedtext": "<strong>Ð\92аÑ\88е ÐºÐ¾Ñ\80иÑ\81ниÑ\87ко Ð¸Ð¼Ðµ Ð¸Ð»Ð¸ IP Ð°Ð´Ñ\80еÑ\81а Ñ\98е Ð±Ð»Ð¾ÐºÐ¸Ñ\80ана.</strong>\n\nÐ\91локиÑ\80аÑ\9aе Ñ\98е {{GENDER:$4|извÑ\80Ñ\88ио|извÑ\80Ñ\88ила}} $1.\nРазлог Ñ\98е <em>$2</em>.\n\n* Ð\9fоÑ\87еÑ\82ак Ð±Ð»Ð¾ÐºÐ¸Ñ\80аÑ\9aа: $8\n* Ð\98Ñ\81Ñ\82ек Ð±Ð»Ð¾ÐºÐ¸Ñ\80аÑ\9aа: $6\n* Ð\91локиÑ\80ани: $7\n\nÐ\9cожеÑ\82е Ð´Ð° Ñ\81е Ð¾Ð±Ñ\80аÑ\82иÑ\82е {{GENDER:$4|коÑ\80иÑ\81никÑ\83|коÑ\80иÑ\81ниÑ\86и}} $1 Ð¸Ð»Ð¸ [[{{MediaWiki:Grouppage-sysop}}|админиÑ\81Ñ\82Ñ\80аÑ\82оÑ\80Ñ\83]] Ñ\80ади Ð´Ð¸Ñ\81кÑ\83Ñ\81иÑ\98е Ð¾ Ð±Ð»Ð¾ÐºÐ¸Ñ\80аÑ\9aÑ\83.\nÐ\9dе Ð¼Ð¾Ð¶ÐµÑ\82е Ð´Ð° ÐºÐ¾Ñ\80иÑ\81Ñ\82иÑ\82е Ð¼Ð¾Ð³Ñ\83Ñ\9bноÑ\81Ñ\82 â\80\9e{{int:emailuser}}â\80\9d Ð¾Ñ\81им Ð°ÐºÐ¾ Ñ\81Ñ\82е Ñ\83нели Ð²Ð°Ð»Ð¸Ð´Ð½Ñ\83 Ð¸Ð¼ÐµÑ\98л Ð°Ð´Ñ\80еÑ\81Ñ\83 Ñ\83 Ñ\81воÑ\98им [[Special:Preferences|подеÑ\88аваÑ\9aима]] Ð½Ð°Ð»Ð¾Ð³Ð° Ð¸ Ð½Ð¸Ñ\81Ñ\82е Ð±Ð»Ð¾ÐºÐ¸Ñ\80ани Ð¾Ð´ ÐºÐ¾Ñ\80иÑ\88Ñ\9bеÑ\9aа Ð¸Ñ\81Ñ\82е.\nÐ\92аÑ\88а Ñ\82Ñ\80енÑ\83Ñ\82на IP Ð°Ð´Ñ\80еÑ\81а Ñ\98е $3, Ð° ID Ð±Ð»Ð¾ÐºÐ¸Ñ\80аÑ\9aа #$5.\nÐ\9dаведиÑ\82е Ñ\81ве Ð¸Ð½Ñ\84оÑ\80маÑ\86иÑ\98е Ð¾Ð´Ð¾Ð·Ð³Ð¾ при стварању било каквих упита.",
        "autoblockedtext": "Ваша IP адреса је блокирана јер ју је употребљавао други корисник, кога је {{GENDER:$4|блокирао|блокирала}} $1.\nРазлог:\n\n:<em>$2</em>\n\n* Датум блокирања: $8\n* Блокирање истиче: $6\n* Име корисника: $7\n\nОбратите се {{GENDER:$4|кориснику|корисници}} $1 или [[{{MediaWiki:Grouppage-sysop}}|администратору]] да разјасните ствар.\n\nНе можете користити могућност „Пошаљи имејл овом кориснику“ ако нисте унели исправну имејл адресу у [[Special:Preferences|подешавањима]].\n\nВаша блокирана IP адреса је $3, а ID $5.\nНаведите све податке изнад при стварању било каквих упита.",
        "blockednoreason": "разлог није наведен",
        "whitelistedittext": "За уређивање странице је потребно да будете $1.",
        "recentchangeslinked-feed": "Сродне измене",
        "recentchangeslinked-toolbox": "Сродне измене",
        "recentchangeslinked-title": "Сродне измене са „$1“",
-       "recentchangeslinked-summary": "УнеÑ\81иÑ\82е Ð¸Ð¼Ðµ Ñ\81Ñ\82Ñ\80аниÑ\86е Ð´Ð° Ð±Ð¸Ñ\81Ñ\82е Ð²Ð¸Ð´ÐµÐ»Ð¸ Ð¿Ñ\80омене Ð½Ð° Ñ\81Ñ\82Ñ\80аниÑ\86ама ÐºÐ¾Ñ\98е Ñ\81Ñ\83 Ð¿Ð¾Ð²ÐµÐ·Ð°Ð½Ðµ Ñ\81а Ð¸Ð»Ð¸ Ñ\81а Ñ\82е Ñ\81Ñ\82Ñ\80аниÑ\86е. (Ð\94а Ð±Ð¸Ñ\81Ñ\82е Ð²Ð¸Ð´ÐµÐ»Ð¸ Ñ\87ланове Ð½ÐµÐºÐµ ÐºÐ°Ñ\82егоÑ\80иÑ\98е, Ñ\83неÑ\81иÑ\82е Ð\9aаÑ\82егоÑ\80иÑ\98а:Ð\9dазив ÐºÐ°Ñ\82егоÑ\80иÑ\98е). Ð\9fÑ\80омене Ð½Ð° Ñ\81Ñ\82Ñ\80аниÑ\86ама ÐºÐ¾Ñ\98е Ñ\81Ñ\83 Ð½Ð° [[Special:Watchlist|ваÑ\88ем Ñ\81пиÑ\81кÑ\83 Ð½Ð°Ð´Ð³Ð»ÐµÐ´Ð°Ñ\9aа]] Ñ\81Ñ\83 '''подебÑ\99ане'''.",
+       "recentchangeslinked-summary": "УнеÑ\81иÑ\82е Ð¸Ð¼Ðµ Ñ\81Ñ\82Ñ\80аниÑ\86е Ð´Ð° Ð±Ð¸Ñ\81Ñ\82е Ð²Ð¸Ð´ÐµÐ»Ð¸ Ð¿Ñ\80омене Ð½Ð° Ñ\81Ñ\82Ñ\80аниÑ\86ама ÐºÐ¾Ñ\98е Ñ\81Ñ\83 Ð¿Ð¾Ð²ÐµÐ·Ð°Ð½Ðµ Ñ\81а Ð¸Ð»Ð¸ Ñ\81а Ñ\82е Ñ\81Ñ\82Ñ\80аниÑ\86е. (Ð\94а Ð±Ð¸Ñ\81Ñ\82е Ð²Ð¸Ð´ÐµÐ»Ð¸ Ñ\87ланове ÐºÐ°Ñ\82егоÑ\80иÑ\98е, Ñ\83неÑ\81иÑ\82е {{ns:category}}:Ð\98ме ÐºÐ°Ñ\82егоÑ\80иÑ\98е). Ð\9fÑ\80омене Ð½Ð° Ñ\81Ñ\82Ñ\80аниÑ\86ама ÐºÐ¾Ñ\98е Ñ\81Ñ\83 Ð½Ð° [[Special:Watchlist|Ð\92аÑ\88ем Ñ\81пиÑ\81кÑ\83 Ð½Ð°Ð´Ð³Ð»ÐµÐ´Ð°Ñ\9aа]] Ñ\81Ñ\83 <strong>подебÑ\99ане</strong>.",
        "recentchangeslinked-page": "Назив странице:",
        "recentchangeslinked-to": "Прикажи измене страница које су повезане с датом страницом",
        "recentchanges-page-added-to-category": "[[:$1]] је додата у категорију",
        "tags": "Важеће ознаке измена",
        "tag-filter": "Филтер за [[Special:Tags|ознаке]]:",
        "tag-filter-submit": "Филтрирај",
-       "tag-list-wrapper": "([[Special:Tags|$1 {{PLURAL:$1|ознака|ознаке|ознака}}]]: $2)",
+       "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Ознака|Ознаке}}]]: $2)",
        "tag-mw-contentmodelchange": "промена модела садржаја",
        "tag-mw-contentmodelchange-description": "Измене које мењају модел садржаја странице",
        "tag-mw-new-redirect": "ново преусмерење",
        "tag-mw-removed-redirect-description": "Измене које мењају постојеће преусмерење у страницу без преусмерења",
        "tag-mw-changed-redirect-target": "промењено одредиште преусмерења",
        "tag-mw-changed-redirect-target-description": "Измене које мењају одредиште преусмерења",
-       "tag-mw-blank": "Страница испражњена",
+       "tag-mw-blank": "страница испражњена",
        "tag-mw-blank-description": "Измене које бришу читав садржај странице",
-       "tag-mw-replace": "Уклоњена већина текста",
+       "tag-mw-replace": "уклоњена већина текста",
        "tag-mw-replace-description": "Измене које уклањају више од 90% садржаја странице",
        "tag-mw-rollback": "враћање",
        "tag-mw-rollback-description": "Измене које враћају страницу на претходне измене",
index fbb0b5a..3a72579 100644 (file)
        "resetpass-submit-loggedin": "Ändra lösenord",
        "resetpass-submit-cancel": "Avbryt",
        "resetpass-wrong-oldpass": "Ogiltigt tillfälligt eller nuvarande lösenord.\nDu kanske redan har lyckats ändra ditt lösenord eller begärt ett nytt tillfälligt lösenord.",
-       "resetpass-recycled": "Var god Ã¥terställ ditt lösenord till någonting annat än ditt aktuella lösenord.",
+       "resetpass-recycled": "Var god Ã¤ndra ditt lösenord till någonting annat än ditt aktuella lösenord.",
        "resetpass-temp-emailed": "Du loggade in med en temporär kod som skickats via e-post.\nFör att slutföra inloggningen måste du ange ett nytt lösenord här:",
        "resetpass-temp-password": "Tillfälligt lösenord:",
        "resetpass-abort-generic": "Lösenordsändring av har avbrutits av ett tillägg.",
        "resetpass-expired": "Ditt lösenord har gått ut. Var god ange ett nytt lösenord för att logga in.",
-       "resetpass-expired-soft": "Ditt lösenord har gÃ¥tt ut och behöver Ã¥terställas. Var god välj ett nytt lösenord nu eller klicka pÃ¥ \"{{int:authprovider-resetpass-skip-label}}\" för att Ã¥terställa det senare.",
-       "resetpass-validity-soft": "Ditt lösenord Ã¤r ogiltigt: $1\n\nVar god välj ett nytt lösenord nu eller klicka pÃ¥ \"{{int:authprovider-resetpass-skip-label}}\" för att Ã¥terställa det senare.",
+       "resetpass-expired-soft": "Ditt lösenord har gÃ¥tt ut och behöver Ã¤ndras. Var god välj ett nytt lösenord nu eller klicka pÃ¥ \"{{int:authprovider-resetpass-skip-label}}\" för att Ã¤ndra det senare.",
+       "resetpass-validity-soft": "Ditt lösenord Ã¤r ogiltigt: $1\n\nVar god välj ett nytt lösenord nu eller klicka pÃ¥ \"{{int:authprovider-resetpass-skip-label}}\" för att Ã¤ndra det senare.",
        "passwordreset": "Återställ lösenord",
        "passwordreset-text-one": "Fyll i detta formulär för att återställa ditt lösenord.",
        "passwordreset-text-many": "{{PLURAL:$1|Fyll i ett av fälten för att få ett tillfälligt lösenord via e-post.}}",
        "rcfilters-other-review-tools": "Andra granskningsverktyg",
        "rcfilters-group-results-by-page": "Gruppera resultat efter sida",
        "rcfilters-activefilters": "Aktiva filter",
+       "rcfilters-activefilters-hide": "Dölj",
+       "rcfilters-activefilters-show": "Visa",
        "rcfilters-advancedfilters": "Avancerade filter",
        "rcfilters-limit-title": "Resultat att visa",
        "rcfilters-limit-and-date-label": "$1 {{PLURAL:$1|ändring|ändringar}}, $2",
        "rcfilters-savedqueries-rename": "Döp om",
        "rcfilters-savedqueries-setdefault": "Ange som standard",
        "rcfilters-savedqueries-unsetdefault": "Ta bort som standard",
-       "rcfilters-savedqueries-remove": "Ta bort",
+       "rcfilters-savedqueries-remove": "Radera",
        "rcfilters-savedqueries-new-name-label": "Namn",
        "rcfilters-savedqueries-new-name-placeholder": "Beskriv syftet med filtret",
        "rcfilters-savedqueries-apply-label": "Skapa filter",
        "rcfilters-empty-filter": "Inga aktiva filter. Alla bidrag visas.",
        "rcfilters-filterlist-title": "Filter",
        "rcfilters-filterlist-whatsthis": "Hur fungerar dessa?",
-       "rcfilters-filterlist-feedbacklink": "Berätta vad du tycker om dessa (nya) filtreringsverktyg",
+       "rcfilters-filterlist-feedbacklink": "Berätta vad du tycker om dessa filtreringsverktyg",
        "rcfilters-highlightbutton-title": "Markera resultat",
        "rcfilters-highlightmenu-title": "Välj en färg",
        "rcfilters-highlightmenu-help": "Välj en färg att markera denna egenskap",
        "speciallogtitlelabel": "Mål (titel eller {{ns:user}}:användarnamn för användare):",
        "log": "Loggar",
        "logeventslist-submit": "Visa",
-       "all-logs-page": "Alla publika loggar",
+       "all-logs-page": "Alla offentliga loggar",
        "alllogstext": "Kombinerad visning av alla tillgängliga loggar för {{SITENAME}}.\nDu kan avgränsa sökningen och få färre träffar genom att ange typ av logg, användarnamn (skiftlägeskänsligt), eller berörd sida (också skiftlägeskänsligt).",
        "logempty": "Inga matchande träffar i loggen.",
        "log-title-wildcard": "Sök efter sidtitlar som börjar med texten",
        "exif-copyrighted-false": "Upphovsrättsstatus inte angivet",
        "exif-photometricinterpretation-0": "Svartvitt (vit är 0)",
        "exif-photometricinterpretation-1": "Svart och vit (svart är 0)",
+       "exif-photometricinterpretation-3": "Palett",
+       "exif-photometricinterpretation-4": "Genomskinlighetsmask",
+       "exif-photometricinterpretation-5": "Separerad (förmodligen CMYK)",
        "exif-unknowndate": "Okänt datum",
        "exif-orientation-1": "Normal",
        "exif-orientation-2": "Spegelvänd horisontellt",
        "tag-mw-undo": "Ångra",
        "tag-mw-undo-description": "Redigeringar som ångrar föregående redigeringar med ångralänken",
        "tags-title": "Märken",
-       "tags-intro": "Denna sida listar de taggar som mjukvaran kan markera en redigering med, och deras betydelse.",
+       "tags-intro": "Denna sida listar de märken som programvaran kan markera en redigering med, och deras betydelse.",
        "tags-tag": "Märkesnamn",
        "tags-display-header": "Utseende på listor över ändringar",
        "tags-description-header": "Full beskrivning av betydelse",
index 9f36f6c..8c7b63a 100644 (file)
@@ -5,7 +5,8 @@
                        "Nemo bis",
                        "Reedy",
                        "아라",
-                       "Matma Rex"
+                       "Matma Rex",
+                       "Raphael Merx"
                ]
        },
        "tog-underline": "Subliña ligasaun sira:",
        "recentchanges-label-newpage": "Pájina foun",
        "recentchanges-label-minor": "Ne'e mudansa ki'ik",
        "recentchanges-label-bot": "Edita husi prosesu automátiku (\"bot\")",
+       "rcfilters-activefilters-hide": "Subar",
+       "rcfilters-activefilters-show": "Hatudu",
+       "rcfilters-savedqueries-remove": "Hapus",
        "rcshowhideminor": "$1 muda ki-ki'ik",
        "rcshowhideminor-show": "Hatudu",
        "rcshowhidebots": "$1 bot sira",
index e0083a5..45c0836 100644 (file)
        "cascadeprotected": "Bu sayfa değişiklik yapılması engellenmiştir, çünkü  \"kademeli\" seçeneği aktif hale getirilerek koruma altına alınan {{PLURAL:$1|sayfada|sayfalarda}} kullanılmaktadır:\n$2",
        "namespaceprotected": "'''$1''' alandındaki sayfaları düzenlemeye izniniz bulunmamaktadır.",
        "customcssprotected": "Bu sayfayı değiştirmeye yetkiniz bulunmamaktadır, çünkü bu sayfa başka bir kullanıcının kişisel ayarlarını içermektedir.",
+       "customjsonprotected": "Başka bir kullanıcının kişisel ayarlarını içerdiği için bu JSON sayfasını düzenleme izniniz yok.",
        "customjsprotected": "Bu Java Script sayfasını değiştirmeye yetkiniz bulunmamaktadır, çünkü bu sayfa başka bir kullanıcının kişisel ayarlarını içermektedir.",
        "mycustomcssprotected": "Bu CSS sayfasını değiştirmeye yetkiniz yok.",
        "mycustomjsonprotected": "Bu JSON sayfasını düzenleme izniniz yok.",
        "botpasswords-label-delete": "Sil",
        "botpasswords-label-resetpassword": "Parolayı sıfırla",
        "botpasswords-label-grants": "Geçerli ayrıcalıklar:",
+       "botpasswords-help-grants": "Hibeler, kullanıcı hesabınızın sahip olduğu haklara erişim izni verir. Burada bir hibe sağlamak, kullanıcı hesabınızın sahip olamayacağı herhangi bir haklara erişim sağlamaz. Daha fazla bilgi için [[Special:ListGrants|hibe tablosuna]] bakınız.",
        "botpasswords-label-grants-column": "Verilen",
        "botpasswords-bad-appid": "Bot ismi \"$1\" geçerli değil.",
        "botpasswords-insert-failed": "Bot adı \"$1\" eklenemedi. Zaten eklenmiş olmalı?",
        "botpasswords-updated-title": "Bot parolası güncellendi",
        "botpasswords-updated-body": "\"$2\" adlı {{GENDER:$2|kullanıcının}} \"$1\" adlı botunun bot parolası güncellendi.",
        "botpasswords-deleted-title": "Bot parolası silindi",
-       "botpasswords-deleted-body": "\"$2\" adlı kullanıcının \"$1\" adlı botunun bot parolası silindi.",
-       "botpasswords-newpassword": "<strong>$1</strong> ile oturum açmak için yeni şifre: <strong>$2</strong>. <em>İlerde başvurmak için lütfen kaydedin.</em>",
+       "botpasswords-deleted-body": "\"$2\" adlı {{GENDER:$2|kullanıcının}} \"$1\" adlı botunun bot parolası silindi.",
+       "botpasswords-newpassword": "<strong>$1</strong> ile giriş yapılan yeni parola: <strong>$2</strong>. <em>Lütfen bunu ileride başvurmak için kaydedin.<em> <br> (Kullanıcı adının, nihai kullanıcı adıyla aynı olmasını gerektiren eski botlar için kullanıcı adı olarak <strong>$3</strong> ve şifre olarak da <strong>$4</strong> kullanabilirsiniz.)",
        "botpasswords-no-provider": "BotPasswordsSessionProvider kullanılamaz.",
        "botpasswords-restriction-failed": "Bot parolası kısıtlamaları bu oturum açma işlemini önlemektedir.",
        "botpasswords-invalid-name": "Belirtilen kullanıcı adı bot parolası ayırıcısı içermiyor (\"$1\").",
+       "botpasswords-not-exist": "\"$1\" kullanıcısının \"$2\" adında bir bot şifresine sahip değil.",
        "botpasswords-needs-reset": "\"$1\" {{GENDER:$1|kullanıcısına}} ait \"$2\" adlı bot için bot parolası sıfırlanmalı.",
        "resetpass_forbidden": "Parolalar değiştirilememektedir",
        "resetpass_forbidden-reason": "Parolalar değiştirilemez: $1",
        "passwordreset-emailelement": "Kullanıcı adı: \n$1\n\nGeçici şifre: \n$2",
        "passwordreset-emailsentemail": "Eğer bu e-posta adresi hesabınızın bağlı olduğu adres ise, bir parola sıfırlama e-postası gönderilecektir.",
        "passwordreset-emailsentusername": "Eğer bu e-posta adresi hesabınızın bağlı olduğu adres ise, bir parola sıfırlama e-postası gönderilecektir.",
+       "passwordreset-nocaller": "Bir ziyaretçi sağlanmalıdır",
+       "passwordreset-nosuchcaller": "Ziyaretçi mevcut değil: $1",
+       "passwordreset-ignored": "Şifre sıfırlama işlenmedi. Belki de herhangi bir sağlayıcı yapılandırılmadı?",
        "passwordreset-invalidemail": "Geçersiz e-posta adresi",
        "passwordreset-nodata": "Ne bir kullanıcı adı ne de bir e-posta adresi verildi.",
        "changeemail": "E-posta adresini değiştir veya çıkar",
        "previewerrortext": "Yaptığınız değişikliklerin önizlemesi sırasında bir hata oluştu.",
        "blockedtitle": "Kullanıcı erişimi engellendi.",
        "blockedtext": "<strong>Kullanıcı adı veya IP adresiniz engellenmiştir.</strong>\n\nSizi engelleyen hizmetli: $1.<br />\nEngelleme sebebi: <em>$2</em>.\n\n* Engellenmenin başlangıcı: $8\n* Engellenmenin bitişi: $6\n* Engellenme süresi: $7\n\nBelirtilen nedene göre engellenmenizin uygun olmadığını düşünüyorsanız, $1 ya da başka bir [[{{MediaWiki:Grouppage-sysop}}|hizmetli]] ile bu durumu görüşebilirsiniz. [[Special:Preferences|Tercihlerim]] kısmında geçerli bir e-posta adresi girmediyseniz \"Kullanıcıya e-posta gönder\" özelliğini kullanamazsınız, tercihlerinize e-posta adresinizi eklediğinizde e-posta gönderme hakkına sahip olacaksınız.\n<br />Şu anki IP adresiniz $3, engellenme numaranız #$5.\n<br />Bir hizmetliden durumunuz hakkında bilgi almak istediğinizde veya herhangi bir sorguda bu bilgiler gerekecektir, lütfen not ediniz.",
-       "autoblockedtext": "IP adresiniz otomatik olarak engellendi, çünkü $1 tarafından engellenmiş başka bir kullanıcı tarafından kullanılmaktaydı.\nBelirtilen sebep şudur:\n\n:''$2''\n\n* Engellemenin başlangıcı: $8\n* Engellemenin bitişi: $6\n* Bloke edilmesi istenen: $7\n\nEngelleme hakkında tartışmak için $1 ile veya diğer [[{{MediaWiki:Grouppage-sysop}}|hizmetlilerden]] biriyle irtibata geçebilirsiniz.\n\nNot, [[Special:Preferences|kullanıcı tercihlerinize]] geçerli bir e-posta adresi kaydetmediyseniz  \"kullanıcıya e-posta gönder\" özelliğinden faydalanamayabilirsiniz ve bu özelliği kullanmaktan engellenmediniz.\n\nŞu anki IP numaranız $3 ve engellenme ID'niz #$5.\nLütfen yapacağınız herhangi bir sorguda yukarıdaki bütün detayları bulundurun.",
+       "autoblockedtext": "IP adresiniz otomatik olarak engellendi, çünkü $1 tarafından engellenmiş başka bir kullanıcı tarafından kullanılmaktaydı.\nBelirtilen sebep şudur:\n\n:<em>$2</em>\n\n* Engellemenin başlangıcı: $8\n* Engellemenin bitişi: $6\n* Bloke edilmesi istenen: $7\n\nEngelleme hakkında tartışmak için $1 ile veya diğer [[{{MediaWiki:Grouppage-sysop}}|hizmetlilerden]] biriyle irtibata geçebilirsiniz.\n\nNot, [[Special:Preferences|kullanıcı tercihlerinize]] geçerli bir e-posta adresi kaydetmediyseniz  \"{{int:emailuser}}\" özelliğinden faydalanamayabilirsiniz ve bu özelliği kullanmaktan engellenmediniz.\n\nŞu anki IP numaranız $3 ve engellenme ID'niz #$5.\nLütfen yapacağınız herhangi bir sorguda yukarıdaki bütün detayları bulundurun.",
+       "systemblockedtext": "Kullanıcı adınız veya IP adresiniz MediaWiki tarafından otomatik olarak engellendi.\nSebebi:\n\n:<em>$2</em>\n\n* Engelin başlangıcı: $8\n* Engelin süresi: $6\n* Engellenmesi istenen: $7\n\nMevcut IP adresiniz $3.\nLütfen yukarıdaki tüm ayrıntıları, yaptığınız sorgularda belirtin.",
        "blockednoreason": "sebep verilmedi",
        "whitelistedittext": "Değişiklik yapabilmek için $1.",
        "confirmedittext": "Sayfa değiştirmeden önce e-posta adresinizi onaylamalısınız. Lütfen [[Special:Preferences|tercihler]] kısmından e-postanızı ekleyin ve onaylayın.",
        "blocked-notice-logextract": "Bu kullanıcı şuanda engellenmiş.\nSon engelleme günlüğü girdisi referans için aşağıda sağlanmıştır:",
        "clearyourcache": "<strong>Note:</strong> Kaydettikten sonra değişiklikleri görmek için tarayıcınızın önbelleğini temizlemeniz gerekebilir.\n* <strong>Firefox / Safari:</strong> <em>Shift</em> tuşuna basılıyken <em>Yeniden yükle</em>'ye tıklayın ya da <em>Ctrl-F5</em> ya da <em>Ctrl-R</em> yapın (Mac için <em>⌘-R</em>).\n* <strong>Google Chrome:</strong> <em>Ctrl-Shift-R</em>'ye basın. (Mac için <em>⌘-Shift-R</em>)\n* <strong>Internet Explorer:</strong> <em>Ctrl</em> basılıyken <em>Yenile</em>'ye tıklayın ya da <em>Ctrl-F5</em> yapın.\n* <strong>Opera:</strong> <em>Araçlar → Tercihler</em>'den önbelliği temizleyin.",
        "usercssyoucanpreview": "'''İpucu:''' Kaydetmeden önce \"{{int:showpreview}}\"e tıklayarak yeni CSSinizi deneyin.",
+       "userjsonyoucanpreview": "<strong>İpucu:</strong> kaydetmeden önce yeni JSON'unuzu test etmek için \"{{int:showpreview}}\" butonunu kullanın.",
        "userjsyoucanpreview": "'''İpucu:''' Kaydetmeden önce \"{{int:showpreview}}\"e tıklayarak yeni JavaScript'inizi test edin.",
        "usercsspreview": "'''Sadece kullanıcı CSS dosyanızın önizlemesini görüyorsun.''' '''Kullanıcı CSS dosyası henüz kaydolmadı!'''",
+       "userjsonpreview": "<strong>Sadece kullanıcı JSON yapılandırmanızı test ettiğiniz/önizleme yaptığınızı unutmayın.\nHenüz kaydedilmedi!</strong>",
        "userjspreview": "'''Sadece test ediyorsun ya da önizleme görüyorsun - kullanıcı JavaScript'i henüz kaydolmadı.'''",
        "sitecsspreview": "'''Sadece kullanıcı CSS dosyanızın önizlemesini görüyorsunuz.''' \n'''Henüz kaydedilmedi!'''",
+       "sitejsonpreview": "<strong>Sadece bu JSON yapılandırmasını önizlediğinizi unutmayın.\nHenüz kaydedilmedi!</strong>",
        "sitejspreview": "'''Sadece kullanıcı JavaScript kod dosyanızın önizlemesini görüyorsunuz.''' \n'''Henüz kaydedilmedi!'''",
-       "userinvalidconfigtitle": "'''Uyarı:''' \"$1\" adında bir tema yoktur. Özel .css ve .js sayfalarının adlarını küçük harf ile yazın, örneğin;  \"{{ns:user}}:Örnek/Vector.css\" yerine \"{{ns:user}}:Örnek/vector.css\" yazın.",
+       "userinvalidconfigtitle": "<strong>Uyarı:</strong> \"$1\" adında bir tema yoktur. Özel .css, .json ve .js sayfalarının adlarını küçük harf ile yazın, örneğin;  \"{{ns:user}}:Örnek/Vector.css\" yerine \"{{ns:user}}:Örnek/vector.css\" yazın.",
        "updated": "(Güncellendi)",
        "note": "'''Not: '''",
        "previewnote": "'''Bunun yalnızca bir ön izleme olduğunu unutmayın.'''\nYaptığınız değişiklikler henüz kaydedilmedi!",
        "continue-editing": "Düzenlemeye devam et",
        "previewconflict": "Bu önizleme metin düzenleme kutucuğunun üstünde, maddenin eğer değişikliklerinizi kaydetmeyi seçerseniz nasıl görüneceğini yansıtır.",
        "session_fail_preview": "Özür dileriz. Oturum verisi kaybından dolayı değişikliğinizi kaydedemedik.\n\nOturumunuzu kapatmış olabilirsiniz. <strong>Lütfen oturumunuzun açık olduğunu doğrulayıp tekrar deneyiniz</strong>.\nEğer sorun devam ederse, [[Special:UserLogout|oturumu kapatıp]] tekrar giriş yapmayı deneyin ve tarayıcınızın bu siteden çerezlere izin verip vermediğini kontrol edin.",
-       "session_fail_preview_html": "'''Üzgünüz! Oturum verisinin kaybolmasından dolayı düzenlemenizi işleme geçiremeyeceğiz.'''\n\n''Çünkü {{SITENAME}} sitesinde raw HTML etkindir, önizleme JavaScript saldırılarına önlem olarak gizlenmiştir.''\n\n'''Eğer bu haklı bir düzenleme girişimiyse, lütfen yeniden deneyin. Eğer hala çalışmazsa, [[Special:UserLogout|çıkış yapıp]] yeniden oturum açmayı deneyin.'''",
+       "session_fail_preview_html": "Üzgünüz! Düzenleme işleminiz, oturum verisi kaybı nedeniyle işlenemedi.\n\n<em>{{SITENAME}} sayfasında ham HTML özelliği etkin olduğundan önizleme, JavaScript saldırılarına karşı bir önlem olarak gizlenir.</em>\n\n<strong>Bu yasal bir düzenleme girişimi ise lütfen tekrar deneyin.</strong>\nHâlâ çalışmıyorsa [[Special:UserLogout|çıkış yapmayı]] ve ardından tekrar giriş yapmayı deneyin ve tarayıcınızın bu sitedeki çerezlere izin verdiğini kontrol edin.",
        "token_suffix_mismatch": "'''Değişikliğiniz geri çevrildi çünkü alıcınız düzenleme kutucuğundaki noktalama işaretlerini bozdu.\nDeğişikliğiniz, sayfa metninde bozulmayı önlemek için geri çevrildi.\nEğer sorunlu bir web-tabanlı anonim proksi servisi kullanıyorsanız bu olay bazen gerçekleşebilir.'''",
        "edit_form_incomplete": "'''Değişiklik formu için bazı sunuculara erişilemedi; yaptığınız değişiklikler bozulmamıştır, gözden geçirip tekrar deneyiniz.'''",
        "editing": "\"$1\" sayfasını değiştirmektesiniz",
        "yourtext": "Sizin metniniz",
        "storedversion": "Kaydedilmiş metin",
        "editingold": "'''Uyarı: Sayfanın eski bir sürümünde değişiklik yapmaktasınız.'''\nKaydettiğinizde bu tarihli sürümden günümüze kadar olan değişiklikler yok olacaktır.",
+       "unicode-support-fail": "Görünüşe göre tarayıcınız Unicode'u desteklemiyor. Sayfaları düzenlemek için bu desteğe ihtiyacınız var. Bundan ötürü düzenlemeniz kaydedilmedi.",
        "yourdiff": "Karşılaştırma",
        "copyrightwarning": "'''Lütfen dikkat:''' {{SITENAME}} sitesine yapılan bütün katkılar $2 sözleşmesi kapsamındadır (ayrıntılar için $1'a bakınız).\nYaptığınız katkının başka katılımcılar tarafından acımasızca değiştirilmesini ve sınırsızca başka yerlere dağıtılmasını istemiyorsanız, katkıda bulunmayınız.<br />\nAyrıca buraya katkıda bulunarak, bu katkının kendiniz tarafından yazıldığına ya da kamuya açık bir kaynaktan ya da başka bir özgür/ücretsiz kaynaktan kopyalandığına güvence vermiş oluyorsunuz. '''Buraya, telif sahibinin izni olmadan telif hakkı ile korunan eserleri eklemeyiz! '''",
        "copyrightwarning2": "Lütfen, {{SITENAME}} sitesine bulunacağınız tüm katkıların diğer üyeler tarafından düzenlenebileceğini, değiştirilebileceğini ya da silinebileceğini hatırlayın. Yazılarınızın merhametsizce değiştirilebilmesine rıza göstermiyorsanız buraya katkıda bulunmayın. <br />\nAyrıca bu ekleyeceğiniz yazıyı sizin yazdığınızı ya da serbest kopyalama izni veren bir kaynaktan kopyaladığınızı bize taahhüt etmektesiniz (ayrıntılar için referans: $1).",
        "editpage-cannot-use-custom-model": "Bu sayfanın içerik modeli değiştirilemez.",
        "longpageerror": "'''Hata: Girdiğiniz metnin uzunluğu kabul edilebilir en fazla uzunluk olan {{PLURAL:$2|bir kilobayt|$2 kilobayt}}tan fazladır ve {{PLURAL:$1|bir kilobayt|$1 kilobayt}} büyüklüğündedir.'''\nDeğişikliğiniz kaydedilemez.",
-       "readonlywarning": "'''Uyarı: Bakım nedeniyle veritabanı şu anda kilitlenmiştir. Bu yüzden şu anda düzenlemelerinizi kaydetmek mümkün değildir.''' \nYaptığınız düzenlemeleri daha sonra kaydetmek isterseniz, yaptığınız düzenlemeleri bir metin dosyasına ya da herhangi bir şeye kopyala yapıştır yaparak saklayınız.\n\nKilitlemeyi yapan yetkili şu açıklamayı eklemiştir: $1",
+       "readonlywarning": "<strong>Uyarı: Bakım nedeniyle veritabanı şu anda kilitlenmiştir. Bu yüzden şu anda düzenlemelerinizi kaydetmek mümkün değildir.</strong> \nYaptığınız düzenlemeleri daha sonra kaydetmek isterseniz, yaptığınız düzenlemeleri bir metin dosyasına ya da herhangi bir şeye kopyala yapıştır yaparak saklayınız.\n\nKilitlemeyi yapan sistem yetkilisi şu açıklamayı eklemiştir: $1",
        "protectedpagewarning": "'''Uyarı: Bu sayfa koruma altına alınmıştır ve yalnızca hizmetli olanlar tarafından değiştirilebilir.'''\nSon günlük girdisi referans amaçlı aşağıda verilmiştir:",
-       "semiprotectedpagewarning": "'''Not:''' Bu sayfa sadece kayıtlı kullanıcı olanlar tarafından değiştirilebilir.\nSon günlük girdisi referans amaçlı aşağıda verilmiştir:",
-       "cascadeprotectedwarning": "'''UYARI:''' Bu sayfa sadece hizmetlilik yetkileri olan kullanıcıların değişiklik yapabileceği şekilde koruma altına alınmıştır. Çünkü  \"kademeli\" seçeneği aktif hale getirilerek koruma altına alınan {{PLURAL:$1|sayfada|sayfada}} kullanılmaktadır:",
+       "semiprotectedpagewarning": "<strong>Not:</strong> Bu sayfa sadece otomatik onaylanmış kullanıcılar tarafından değiştirilebilir.\nSon günlük girdisi referans amaçlı aşağıda verilmiştir:",
+       "cascadeprotectedwarning": "<strong>Uyarı:</strong> Bu sayfa, yalnızca [[Special:ListGroupRights|belirli haklara]] sahip kullanıcıların bunu düzenleyebilmesi için korunmuştur çünkü \"kademeli\" seçeneği aktif hale getirilerek koruma altına alınan {{PLURAL:$1|sayfada|sayfada}} kullanılmaktadır:",
        "titleprotectedwarning": "'''Uyarı: Bu sayfa [[Special:ListGroupRights|özel hakları]] olanların oluşturabilmeleri için kilitlenmiştir.'''\nSon günlük girdisi referans amaçlı aşağıda verilmiştir:",
        "templatesused": "Bu sayfada kullanılan {{PLURAL:$1|şablon|şablonlar}}:",
        "templatesusedpreview": "Bu önizlemede kullanılan {{PLURAL:$1|şablon|şablonlar}}:",
        "permissionserrors": "İzin hatası",
        "permissionserrorstext": "Aşağıdaki {{PLURAL:$1|sebep|sebepler}}den dolayı, bunu yapmaya yetkiniz yok:",
        "permissionserrorstext-withaction": "Aşağıdaki {{PLURAL:$1|neden|nedenler}}den dolayı $2 yetkiniz yok:",
+       "contentmodelediterror": "Bu revizyonu, içerik modeli <code>$1</code> olduğu için düzenleyemezsiniz. <code>$2</code> sayfasının güncel içerik modelinden farklıdır",
        "recreate-moveddeleted-warn": "<strong>Uyarı: Daha önceden silinmiş bir sayfayı tekrar oluşturuyorsunuz.</strong>\n\nBu sayfayı düzenlemeye devam etmenin uygun olup olmadığını düşünmelisiniz.\nBu sayfanın silme ve taşıma günlüğü kolaylık için burada verilmiştir:",
        "moveddeleted-notice": "Bu sayfa silinmiş.\nSayfanın silme, koruma ve taşıma kaydı referans için aşağıda verilmiştir.",
+       "moveddeleted-notice-recent": "Üzgünüz, bu sayfa yakın zamanda silinmiştir (son 24 saat içinde).\nSayfa için silme, koruma ve taşıma kaydı referans amacıyla aşağıda verilmiştir.",
        "log-fulllog": "Tam günlüğü gör",
        "edit-hook-aborted": "Değişiklik çengelle durduruldu.\nBir açıklama verilmedi.",
        "edit-gone-missing": "Sayfa güncellenemiyor.\nSilinmiş görünüyor.",
        "content-json-empty-object": "Boş nesne",
        "content-json-empty-array": "Boş dizi",
        "deprecated-self-close-category": "Kendiliğinden geçersiz HTML etiketlerini kullanan sayfalar",
+       "deprecated-self-close-category-desc": "Sayfa, <code>&lt;b/></code> veya <code>&lt;span/></code> gibi kendiliğinden geçersiz HTML etiketleri içeriyor. Bunların davranışları yakında HTML5 belirtimiyle tutarlı olacak şekilde değişecektir, bu nedenle wikitext'deki kullanımları, kullanımdan kaldırılır.",
        "duplicate-args-warning": "<strong>Uyarı:</strong>[[:$1]] [[:$2]] şablonunu \"$3\" parametresi için birden fazla değerle çağırıyor. Sadece sağlanan son değer kullanılacak.",
        "duplicate-args-category": "Yinelenen şablon değişkenleri kullanan sayfalar",
        "duplicate-args-category-desc": "Sayfada içeren şablonları çağırmak için bu terimler kullanılır <code><nowiki>{{foo|bar=1|bar=2}}</nowiki></code> or <code><nowiki>{{foo|bar|1=baz}}</nowiki></code>.",
        "post-expand-template-argument-category": "Geçersiz şablon değiştirgenleri içeren sayfalar",
        "parser-template-loop-warning": "Şablon düğümü tespit edildi: [[$1]]",
        "template-loop-category": "Şablon döngülü sayfalar",
+       "template-loop-category-desc": "Sayfa bir şablon döngüsü içermektedir. Örneğin, kendini sürekli olarak çağıran bir şablon.",
+       "template-loop-warning": "<strong>Uyarı:</strong> Bu sayfa, bir şablon döngüsüne (sonsuz yinelemeli çağrı) neden olan [[:$1]] şablonunu çağırmaktadır.",
        "parser-template-recursion-depth-warning": "Şablon özyineleme yoğunluğu sınırı aşıldı ($1)",
        "language-converter-depth-warning": "Dil çevirici derinlik sınırı aşıldı ($1)",
        "node-count-exceeded-category": "Düğüm sayısı aşılan sayfalar",
        "expansion-depth-exceeded-warning": "Sayfa genişletme derinliği aşıldı",
        "parser-unstrip-loop-warning": "Yineleme döngüsü algılandı",
        "unstrip-depth-warning": "($1) yineleme sınırı aşıldı",
+       "unstrip-depth-category": "Sızıntı derinliği sınırının aşıldığı sayfalar",
        "converter-manual-rule-error": "Elle yapılandırma dil dönüşüm kuralı hatası tespit edildi",
        "undo-success": "Bu değişiklik geri alınabilir. Lütfen aşağıdaki karşılaştırmayı kontrol edin, gerçekten bu değişikliği yapmak istediğinizden emin olun ve sayfayı kaydederek bir önceki değişikliği geriye alın.",
        "undo-failure": "Değişikliklerin çakışması nedeniyle geri alma işlemi başarısız oldu.",
        "mw-widgets-titleinput-description-new-page": "sayfa henüz mevcut değil",
        "mw-widgets-titleinput-description-redirect": "$1'e yönlendirildi",
        "mw-widgets-categoryselector-add-category-placeholder": "Bir kategori ekle...",
+       "mw-widgets-usersmultiselect-placeholder": "Daha fazla ekle...",
        "date-range-from": "Şu tarihten:",
        "date-range-to": "Bu güne kadar:",
        "sessionprovider-mediawiki-session-cookiesessionprovider": "çerez tabanlı oturumlar",
index 97b7b83..ba46946 100644 (file)
@@ -55,7 +55,7 @@
        "tog-enotifwatchlistpages": "Күзәтү исемлегемдәге бит яки файл үзгәртелү турында электрон почтага хәбәр җибәрелсен",
        "tog-enotifusertalkpages": "Бәхәс битем үзгәртелү турында электрон почтага хәбәр җибәрелсен",
        "tog-enotifminoredits": "Кече үзгәртүләр турында да электрон почтага хәбәр җибәрелсен",
-       "tog-enotifrevealaddr": "Ð¥Ó\99бÓ\99Ñ\80лÓ\99Ñ\80дÓ\99 e-mail адресым күрсәтелсен",
+       "tog-enotifrevealaddr": "Ð¥Ó\99бÓ\99Ñ\80намÓ\99дÓ\99 Ð¼Ð¸Ð½ÐµÐ¼ Ð¿Ð¾Ñ\87Ñ\82а адресым күрсәтелсен",
        "tog-shownumberswatching": "Битне күзәтү исемлекләренә өстәгән кулланучылар санын күрсәтелсен",
        "tog-oldsig": "Хәзерге имзагыз:",
        "tog-fancysig": "Имзаның шәхси вики-билгеләмәсе (автоматик сылтамасыз)",
        "rcfilters-savedqueries-add-new-title": "Хәзерге фильтр көйләнмәләрен саклау",
        "rcfilters-clear-all-filters": "Барлык филтерләрне чистарту",
        "rcfilters-search-placeholder": "Фильтрланы соңгы үзгәртү (карау яисә кертүне башлау)",
-       "rcfilters-filterlist-feedbacklink": "Әлеге (яңа) фильтрлау кораллары турында турында фикер калдырыгыз",
+       "rcfilters-filterlist-feedbacklink": "Әлеге фильтрлау кораллары турында турында фикер калдырыгыз",
        "rcfilters-filtergroup-authorship": "Үзгәртүләрнең авторлыгы",
        "rcfilters-filter-editsbyself-label": "Сезнең үзгәртүләр",
        "rcfilters-filter-editsbyself-description": "Сезнең кертемегез.",
index b3216a6..65c4cc8 100644 (file)
@@ -37,7 +37,8 @@
                        "Zainab Meher",
                        "Sayam Asjad",
                        "Abdulq",
-                       "Fitoschido"
+                       "Fitoschido",
+                       "Dcljr"
                ]
        },
        "tog-underline": "ربط کی خط کشیدگی:",
        "enotif_subject_moved": "{{SITENAME}} میں صفحہ $1 کو $2 نے {{GENDER:$2|منتقل کیا}}",
        "enotif_subject_restored": "{{SITENAME}} میں صفحہ $1 کو $2 نے {{GENDER:$2|بحال کیا}}",
        "enotif_subject_changed": "{{SITENAME}} کے صفحہ $1 میں $2 نے {{GENDER:$2|تبدیلی کی}}",
-       "enotif_body_intro_deleted": "{{SITENAME}} میں صفحہ $1 کو بتاریخ $PAGEEDITDATE صارف $2 نے {{GENDER:$2|حذف کر دیا}} ہے، ملاحظہ فرمائیں $3۔",
+       "enotif_body_intro_deleted": "{{SITENAME}} میں صفحہ $1 کو بتاریخ $PAGEEDITDATE صارف $2 نے {{GENDER:$2|حذف کر دیا}} ہے، ملاحظہ فرمائیں $3 ۔",
        "enotif_body_intro_created": "{{SITENAME}} میں صفحہ $1 کو بتاریخ $PAGEEDITDATE صارف $2 نے {{GENDER:$2|تخلیق کیا}}، موجودہ نسخہ دیکھنے کے لیے $3 ملاحظہ فرمائیں۔",
        "enotif_body_intro_moved": "{{SITENAME}} میں صفحہ $1 کو بتاریخ $PAGEEDITDATE صارف $2 نے {{GENDER:$2|منتقل کر دیا}} ہے، موجودہ نسخہ دیکھنے کے لیے $3 ملاحظہ فرمائیں۔",
        "enotif_body_intro_restored": "{{SITENAME}} میں صفحہ $1 کو بتاریخ $PAGEEDITDATE صارف $2 نے {{GENDER:$2|بحال کیا}}، موجودہ نسخہ دیکھنے کے لیے $3 ملاحظہ فرمائیں۔",
index bad9d86..a05f234 100644 (file)
        "rcfilters-savedqueries-rename": "Đổi tên",
        "rcfilters-savedqueries-setdefault": "Đặt làm mặc định",
        "rcfilters-savedqueries-unsetdefault": "Loại bỏ mặc định",
-       "rcfilters-savedqueries-remove": "Loại bỏ",
+       "rcfilters-savedqueries-remove": "Xóa",
        "rcfilters-savedqueries-new-name-label": "Tên",
        "rcfilters-savedqueries-new-name-placeholder": "Miêu tả mục đích của bộ lọc",
        "rcfilters-savedqueries-apply-label": "Tạo bộ lọc",
        "rcfilters-empty-filter": "Không có bộ lọc hiện hành. Tất cả các đóng góp được hiển thị.",
        "rcfilters-filterlist-title": "Bộ lọc",
        "rcfilters-filterlist-whatsthis": "Chúng hoạt động làm sao?",
-       "rcfilters-filterlist-feedbacklink": "Cho chúng tôi biết bạn cảm thấy sao về các công cụ bộ lọc mới này",
+       "rcfilters-filterlist-feedbacklink": "Hãy cho chúng tôi biết bạn cảm thấy sao về các công cụ bộ lọc này",
        "rcfilters-highlightbutton-title": "Làm nổi bật kết quả",
        "rcfilters-highlightmenu-title": "Chọn màu",
        "rcfilters-highlightmenu-help": "Chọn màu để làm nổi bật thuộc tính này",
        "dellogpage": "Nhật trình xóa",
        "dellogpagetext": "Dưới đây là danh sách các trang bị xóa gần đây nhất.",
        "deletionlog": "nhật trình xóa",
+       "logentry-create-create": "$1 {{GENDER:$2|đã tạo}} trang $3",
        "reverted": "Đã hồi phục một phiên bản cũ",
        "deletecomment": "Lý do:",
        "deleteotherreason": "Lý do khác/bổ sung:",
index bbff36b..7f670e1 100644 (file)
        "recentchangeslinked-feed": "Mga may kalabotan nga binag-o",
        "recentchangeslinked-toolbox": "Mga may kalabotan nga binag-o",
        "recentchangeslinked-title": "Mga pagbag-o kasumpay ha ''$1''",
-       "recentchangeslinked-summary": "Ini nga taramdan hin pagbag-o nga lab-as nga hinimo ha mga pakli nga nakasumpay tikang a naka-specifico nga pakli (o ha api han uska specifico nga kaarangay).\nMga pakli ha [[Special:Watchlist|imo angay timan-an]] in naka-'''bold'''.",
+       "recentchangeslinked-summary": "Ibutang an ngaran han pakli basi maipakita an mga pakli nga nakasumapy pakadto o pakadi hinin nga pakli. (Basi makit-an an mga api hini nga kaarangay, ibutang an {{ns:category}}:Ngaran han kaarangay). An mga binalyuan ha mga pakli ha [[Special:Watchlist|your Watchlist]] nakasurat nga <strong>madakmol</strong>.",
        "recentchangeslinked-page": "Ngaran han pakli:",
        "recentchangeslinked-to": "Igpakita lugod an mga pagbabag-o han mga pakli nga nahisumpay ha ginhatag nga pakli",
        "upload": "Pagkarga hin file",
index edde6a1..fa87948 100644 (file)
        "resetpass-submit-loggedin": "טוישן פאסווארט",
        "resetpass-submit-cancel": "אַנולירן",
        "resetpass-wrong-oldpass": "אומגילטיג צײַטווײַליק אדער לויפֿיק פאַסווארט.\nאיר האט מעגלעך שוין געטוישט אייער פאַסווארט אדער געבעטן א נײַ  צײַטווײַליק פאַסווארט.",
-       "resetpass-recycled": "×\96ײַ×\98 ×\90×\96×\95×\99 ×\92×\95×\98 ×©×\98×¢×\98×\9c ×\90×\99רע ×¤×\90ס×\95×\95×\90ר×\98 ×¦×\95 ×¢×¤×¢×¡ ×\90× ×\93ערש ×¤×\95× ×¢×\9d ×\9c×\95×\99פ×\99ק×\9f ×¤×\90ס×\95×\95×\90רט.",
+       "resetpass-recycled": "×\96×\99×\99×\98 ×\90Ö·×\96×\95×\99 ×\92×\95×\98, ×\91×\99×\99×\98 ×\90×\99×\99ער ×¤Ö¼×\90ַס×\95×\95×\90ָר×\98 ×\90×\95×\99×£ ×¢×¤Ö¼×¢×¡ ×\90Ö·× ×\93ערש ×¤×\95×\9f ×\9c×\95×\99פ×\99ק×\9f ×¤Ö¼×\90ַס×\95×\95×\90Ö¸רט.",
        "resetpass-temp-emailed": "איר האט זיך ארי לאגירת מיט א פראוויזארישן קאד געשיקט דורכן ע־פאסט. כדי שליסן דאס ארײַנלאגירן, דארט איר שטעלן א נײַ פאסווארט דא.",
        "resetpass-temp-password": "צײַטווייליק פאַסווארט:",
        "resetpass-abort-generic": "פאסווארט ענדערונג איז מבוטל געווארן דורך א פארברייטערונג.",
        "resetpass-expired": "אירע פאסווארט איז אויסגעגאנגען. זײַט אזוי גוט שטעלט א נײַ פאסווארט כדי ארײַנלאגירן.",
-       "resetpass-expired-soft": "×\90ײַער ×¤×\90ס×\95×\95×\90ר×\98 ×\90×\99×\96 ×\90×\95×\99ס×\92×¢×\92×\90× ×\92×¢×\9f ×\90×\95×\9f ×\93×\90רף ×\95×\95ער×\9f ×¦×\95ר×\99ק×\92עש×\98×¢×\9c×\98. ×\96ײַ×\98 ×\90×\96×\95×\99 ×\92×\95×\98 ×§×\9c×\95×\99×\91×\98 ×\90 × ×²Ö· ×¤×\90ס×\95×\95×\90ר×\98 ×\90צ×\99× ×\93, ×\90×\93ער ×§×\9c×\99ק×\98 \"{{int:authprovider-resetpass-skip-label}}\" ×\9b×\93×\99 ×¢×¡ ×¦×\95ר×\99קש×\98×¢×\9c×\9f ×©×¤עטער.",
-       "resetpass-validity-soft": "×\90ײַער ×¤×\90ס×\95×\95×\90ר×\98 ×\90×\99×\96 × ×\99ש×\98 ×\92×\99×\9c×\98×\99ק: $1\n\n×\96ײַ×\98 ×\90×\96×\95×\99 ×\92×\95×\98 ×§×\9c×\95×\99×\91×\98 ×\90 × ×²Ö· ×¤×\90ס×\95×\95×\90ר×\98 ×\90צ×\99× ×\93, ×\90×\93ער ×§×\9c×\99ק×\98 \"{{int:authprovider-resetpass-skip-label}}\" ×\9b×\93×\99 ×¢×¡ ×¦×\95ר×\99קש×\98×¢×\9c×\9f ×©×¤עטער.",
+       "resetpass-expired-soft": "×\90×\99×\99ער ×¤Ö¼×\90ַס×\95×\95×\90ָר×\98 ×\90×\99×\96 ×\90×\95×\99ס×\92×¢×\92×\90Ö·× ×\92×¢×\9f ×\90×\95×\9f ×\93×\90ַרף ×¦×\95ר×\99ק×\92עש×\98×¢×\9c×\98 ×\95×\95ער×\9f. ×\96×\99×\99×\98 ×\90Ö·×\96×\95×\99 ×\92×\95×\98, ×§×\9c×\99×\99×\91×\98 ×\90Ö· × ×\99×\99 ×¤Ö¼×\90ַס×\95×\95×\90ָר×\98 ×\90ַצ×\99× ×\93, ×\90Ö¸×\93ער ×§×\9c×\99ק×\98 \"{{int:authprovider-resetpass-skip-label}}\" ×\9b×\93×\99 ×¦×\95 ×\91×\99×\99×\98×\9f ×¢×¡ ×©×¤Ö¼עטער.",
+       "resetpass-validity-soft": "×\90×\99×\99ער ×¤Ö¼×\90ַס×\95×\95×\90ָר×\98 ×\90×\99×\96 × ×\99×\98 ×\92×\99×\9c×\98×\99ק: $1\n\n×\96×\99×\99×\98 ×\90Ö·×\96×\95×\99 ×\92×\95×\98 ×§×\9c×\99×\99×\91×\98 ×\90Ö· × ×\99×\99 ×¤Ö¼×\90ַס×\95×\95×\90ָר×\98 ×\90ַצ×\99× ×\93, ×\90Ö¸×\93ער ×§×\9c×\99ק×\98 \"{{int:authprovider-resetpass-skip-label}}\" ×\9b×\93×\99 ×¦×\95 ×\91×\99×\99×\98×\9f ×¢×¡ ×©×¤Ö¼עטער.",
        "passwordreset": "צוריקשטעלן פאַסווארט",
        "passwordreset-text-one": "דערגאַנצט די פאָרעם צוריקצושטעלן אײַער פאַסווארט.",
        "passwordreset-text-many": "{{PLURAL:$1|דערגאַנצט איינע פֿון די פֿעלדער כדי צו באקומען א פראוויזאריש פאַסווארט דור כן פאסט.}}",
        "rcfilters-savedqueries-defaultlabel": "אױפֿגעהיטענע פֿילטערס",
        "rcfilters-savedqueries-rename": "ענדערן נאמען",
        "rcfilters-savedqueries-setdefault": "האלטן ווי גרונט",
-       "rcfilters-savedqueries-remove": "×\90ר×\90פנע×\9e×¢ן",
+       "rcfilters-savedqueries-remove": "×\90×\95×\99ס×\9eעקן",
        "rcfilters-savedqueries-new-name-label": "נאָמען",
        "rcfilters-savedqueries-apply-label": "שאפן פילטער",
        "rcfilters-savedqueries-cancel-label": "אַנולירן",
        "rcfilters-empty-filter": "קיין אַקטיווע פילטערס. אלע ביישטייערונגען געוויזן.",
        "rcfilters-filterlist-title": "פֿילטערס",
        "rcfilters-filterlist-whatsthis": "ווי ארבעט דאס?",
+       "rcfilters-filterlist-feedbacklink": "זאָגט אונדז וואָס איז אייער מיינונג בנוגע פילטער אינסטרומענטן",
        "rcfilters-highlightbutton-title": "ארויסשטאַרצן רעזולטאַטן",
        "rcfilters-highlightmenu-title": "אויסקלויבן א קאליר",
        "rcfilters-filterlist-noresults": "קיין פֿילטערס נישט געטראפֿן",
        "log-action-filter-protect-unprotect": "אראפנעמען שיץ",
        "authmanager-userdoesnotexist": "באניצער קאנטע \"$1\" איז נישט איינגעשריבן.",
        "authmanager-realname-label": "עכטער נאָמען",
+       "authprovider-resetpass-skip-label": "איבערהיפן",
        "revid": "רעוויזיע $1"
 }
index 297bd33..2ed7c43 100644 (file)
        "search-section": "(ⴰⵙⴱⴹⵓ $1)",
        "search-suggest": "ⵉⵙ ⵜⵅⵙⴷ ⴰⴷ ⵜⵉⵏⵉⴷ: $1",
        "search-interwiki-more": "(ⵓⴳⴳⴰⵔ)",
-       "searchall": "ⴰⴽⴽ",
+       "searchall": "âµ\8eâ´°âµ\95âµ\95â´°",
        "search-nonefound": "ⵓⵔ ⵍⵍⵉⵏⵜ ⵜⵢⴰⴼⵓⵜⵉⵏ ⵉⵎⵙⴰⵙⴰⵏ ⴷ ⵓⵙⵓⵜⵔ.",
+       "powersearch-toggleall": "ⵎⴰⵕⵕⴰ",
        "mypreferences": "ⵉⵙⵎⵏⵢⵉⴼⵏ",
        "prefs-watchlist-days": "ⵓⵙⵙⴰⵏ ⵜⵓⵙⴽⵏⵉⵏ ⴳ ⵓⵎⵓⵖ ⵓⵙⵎⵓⵇⵇⵍ",
        "recentchangesdays": "ⵓⵙⵙⴰⵏ ⵜⵓⵙⴽⵏⵉⵏ ⴳ ⵉⵙⵏⴼⵉⵍⵏ ⵉⵎⴳⴳⵓⵔⴰ",
        "prefs-signature": "ⴰⵙⴳⵎⴹ",
        "userrights-reason": "ⵜⴰⵎⵏⵜⵉⵍⵜ:",
        "group-sysop": "ⵉⵏⵎⵀⴰⵍⵏ",
+       "group-all": "(ⵎⴰⵕⵕⴰ)",
        "right-edit": "ⵙⵏⴼⵍ ⵜⴰⵙⵏⵉⵡⵉⵏ",
        "right-upload": "ⵙⴽⵜⵔ ⵉⴼⴰⵢⵍⵓⵜⵏ",
        "right-upload_by_url": "ⵙⴽⵜⵔ ⴰⴼⴰⵢⵍⵓ ⵙⴳ URL",
        "booksources-search-legend": "ⵔⵣⵓ ⵅⴼ ⵉⴷⵍⵉⵙⵏ ⵏ ⵓⵙⴰⴳⵎ",
        "booksources-search": "ⵔⵣⵓ",
        "log": "ⵉⵣⵎⵎⴻⵎⵏ",
+       "checkbox-all": "ⵎⴰⵕⵕⴰ",
        "allpages": "ⵎⴰⵕⵕⴰ ⵜⴰⵙⵏⵉⵡⵉⵏ",
        "allarticles": "ⵜⴰⵙⵏⵉⵡⵉⵏ ⴰⴽⴽ",
        "allpagessubmit": "ⴷⴷⵓ",
        "categories": "ⵉⵙⵎⵉⵍⵏ",
        "sp-deletedcontributions-contribs": "ⵜⵓⵎⵓⵜⵉⵏ",
        "listgrouprights-members": "ⵜⴰⵍⴳⴰⵎⵜ ⵏ ⵉⴳⵎⴰⵎⵏ",
+       "emailmessage": "ⵜⵓⵣⵉⵏⵜ:",
        "usermessage-editor": "ⵓⴷⵓⵙ ⵏ ⵓⵎⵢⴰⵣⴰⵏ",
        "watchlist": "ⵜⴰⵍⴳⴰⵎⵜ ⵏ ⵓⴹⴼⴼⵓⵔ",
        "mywatchlist": "ⵜⴰⵍⴳⴰⵎⵜ ⵏ ⵓⴹⴼⴼⵓⵔ",
        "delete_and_move_confirm": "ⵢⴰⵀ, ⴽⴽⵙ ⵜⴰⵙⵏⴰ",
        "export": "ⵙⵙⵓⴼⵖ ⵜⴰⵙⵏⵉⵡⵉⵏ",
        "allmessagesname": "ⵉⵙⵎ",
+       "allmessages-filter-all": "ⵎⴰⵕⵕⴰ",
        "allmessages-language": "ⵜⵓⵜⵍⴰⵢⵜ:",
        "allmessages-filter-translate": "ⵙⵙⵓⵖⵍ",
        "thumbnail-more": "ⵙⵙⵉⵎⵖⵓⵔ",
        "watchlisttools-clear": "ⵙⴼⴹ ⵜⴰⵍⴳⴰⵎⵜ ⵏ ⵓⴹⴼⴼⵓⵔ",
        "watchlisttools-view": "ⵙⴽⵏ ⵉⵙⵏⵉⴼⵉⵍⵏ ⴷ ⵢⵓⵙⴰⵏ",
        "watchlisttools-edit": "ⵥⵕ ⴷ ⵜⵙⵏⴼⵍⴷ ⵜⴰⵍⴳⴰⵎⵜ ⵏ ⵓⴹⴼⴼⵓⵔ",
+       "version-poweredby-translators": "ⵉⵎⵙⵖⵓⵍⵏ ⵏ translatewiki.net",
        "redirect-submit": "ⴷⴷⵓ",
        "redirect-value": "ⴰⵣⴰⵍ",
        "redirect-user": "ID ⵏ ⵓⵎⵙⵙⵎⵔⵙ",
        "logentry-newusers-create": "{{GENDER:$2|ⵉⵙⵏⴼⵍ ⵓⵏⵙⵙⵎⵔⵙ|ⵜⵙⵏⴼⵍ ⵜⵏⵙⵙⵎⵔⵙⵜ}} $1 ⴰⵎⵉⴹⴰⵏ ⵏⵙ",
        "logentry-upload-upload": "{{GENDER:$2|ⵉⵙⴽⵜⵔ|ⵜⵙⴽⵜⵔ}} $1 $3",
        "rightsnone": "(ⵓⵍⴰ ⵢⴰⵏ)",
+       "feedback-message": "ⵜⵓⵣⵉⵏⵜ:",
        "feedback-thanks-title": "ⵜⴰⵏⵎⵎⵉⵔⵜ!",
        "searchsuggest-search": "ⵔⵣⵓ ⴳ {{SITENAME}}",
        "duration-days": "$1 ⵏ {{PLURAL:$1|ⵡⴰⵙⵙ|ⵡⵓⵙⵙⴰⵏ}}",
index 5bb1a43..0bc666c 100644 (file)
        "resetpass-submit-loggedin": "更改密码",
        "resetpass-submit-cancel": "取消",
        "resetpass-wrong-oldpass": "临时密码或当前密码无效。您可能已经更改了您的密码,或者请求了新的临时密码。",
-       "resetpass-recycled": "请重置您的密码为与当前密码不同的密码。",
+       "resetpass-recycled": "请更改您的密码为与当前密码不同的密码。",
        "resetpass-temp-emailed": "您使用了通过电子邮件发送的临时密码登录。要完成登录,您必须在此设置一个新密码:",
        "resetpass-temp-password": "临时密码:",
        "resetpass-abort-generic": "密码更改已经被扩展程序中止。",
        "resetpass-expired": "您的密码已经到期。请设置新登录密码。",
-       "resetpass-expired-soft": "您的密码已经到期,需要重置。请现在更换新密码,或单击“{{int:authprovider-resetpass-skip-label}}”以稍后重置。",
-       "resetpass-validity-soft": "您的密码无效:$1\n\n请选择一个新密码,或单击“{{int:authprovider-resetpass-skip-label}}”以稍后重置。",
+       "resetpass-expired-soft": "您的密码已经到期,需要更改。请现在更换新密码,或单击“{{int:authprovider-resetpass-skip-label}}”以稍后更改。",
+       "resetpass-validity-soft": "您的密码无效:$1\n\n请选择一个新密码,或单击“{{int:authprovider-resetpass-skip-label}}”以稍后更改。",
        "passwordreset": "重置密码",
        "passwordreset-text-one": "请完成此表单来通过电子邮件接收临时密码。",
        "passwordreset-text-many": "{{PLURAL:$1|填写其中一个字段以通过电子邮件接收临时密码。}}",
        "rcfilters-other-review-tools": "其他复核工具",
        "rcfilters-group-results-by-page": "按页面分组结果",
        "rcfilters-activefilters": "应用的过滤器",
+       "rcfilters-activefilters-hide": "隐藏",
+       "rcfilters-activefilters-show": "显示",
        "rcfilters-advancedfilters": "高级过滤器",
        "rcfilters-limit-title": "要显示的结果",
        "rcfilters-limit-and-date-label": "$1次{{PLURAL:$1|更改}},$2",
        "rcfilters-savedqueries-rename": "重命名",
        "rcfilters-savedqueries-setdefault": "设置为默认",
        "rcfilters-savedqueries-unsetdefault": "移除为默认",
-       "rcfilters-savedqueries-remove": "除",
+       "rcfilters-savedqueries-remove": "除",
        "rcfilters-savedqueries-new-name-label": "名称",
        "rcfilters-savedqueries-new-name-placeholder": "描述过滤器目的",
        "rcfilters-savedqueries-apply-label": "创建过滤器",
        "rcfilters-empty-filter": "没有应用的过滤器。所有贡献都已显示。",
        "rcfilters-filterlist-title": "过滤器",
        "rcfilters-filterlist-whatsthis": "这些是怎样工作的?",
-       "rcfilters-filterlist-feedbacklink": "告诉我们您对这些(新)过滤工具有什么想法",
+       "rcfilters-filterlist-feedbacklink": "告诉我们您对这些过滤工具有什么想法",
        "rcfilters-highlightbutton-title": "高亮结果",
        "rcfilters-highlightmenu-title": "选择颜色",
        "rcfilters-highlightmenu-help": "选择颜色来高亮该属性",
index 40d7f2b..626a454 100644 (file)
        "resetpass-submit-loggedin": "變更密碼",
        "resetpass-submit-cancel": "取消",
        "resetpass-wrong-oldpass": "無效的臨時或現有密碼。\n您可能已經變更了密碼,或者已經請求一個新的臨時密碼。",
-       "resetpass-recycled": "請重設您的密碼為一個與目前不同的密碼。",
+       "resetpass-recycled": "請將您的密碼更改成與目前內容不同的密碼。",
        "resetpass-temp-emailed": "您使用臨時電子郵件傳送的密碼登入。\n要完成登入,您必須在這裡設定一個新密碼:",
        "resetpass-temp-password": "臨時密碼:",
        "resetpass-abort-generic": "擴充套件已中止了變更密碼的操作。",
        "resetpass-expired": "您的密碼已過期。請設定一個新的密碼以登入。",
-       "resetpass-expired-soft": "您的密碼已過期,需要重新設定。請現在設定您的新密碼,或點選 \"{{int:authprovider-resetpass-skip-label}}\" 稍後再重設。",
-       "resetpass-validity-soft": "您的密碼無效:$1 \n\n請現在設定您的新密碼,或點選 \"{{int:authprovider-resetpass-skip-label}}\" 稍後再重設。",
+       "resetpass-expired-soft": "您的密碼已過期,需要作出更改。請現在設定您的新密碼,或點選 \"{{int:authprovider-resetpass-skip-label}}\" 來事後更改。",
+       "resetpass-validity-soft": "您的密碼無效:$1 \n\n請現在設定您的新密碼,或點選 \"{{int:authprovider-resetpass-skip-label}}\" 來事後更改。",
        "passwordreset": "重新設定密碼",
        "passwordreset-text-one": "完成此表單,透過電子郵件傳送臨時密碼以重新設定您的密碼。",
        "passwordreset-text-many": "{{PLURAL:$1|填寫其中一個欄位以透過電子郵件接收臨時密碼。}}",
        "rcfilters-other-review-tools": "其他檢閱工具",
        "rcfilters-group-results-by-page": "按頁面分組結果",
        "rcfilters-activefilters": "使用的篩選器",
+       "rcfilters-activefilters-hide": "隱藏",
+       "rcfilters-activefilters-show": "顯示",
        "rcfilters-advancedfilters": "進階查詢條件",
        "rcfilters-limit-title": "要顯示的結果",
        "rcfilters-limit-and-date-label": "$1次{{PLURAL:$1|變更}},$2",
        "rcfilters-savedqueries-rename": "重新命名",
        "rcfilters-savedqueries-setdefault": "設為預設",
        "rcfilters-savedqueries-unsetdefault": "取消設為預設",
-       "rcfilters-savedqueries-remove": "除",
+       "rcfilters-savedqueries-remove": "除",
        "rcfilters-savedqueries-new-name-label": "名稱",
        "rcfilters-savedqueries-new-name-placeholder": "說明查詢條件的用途",
        "rcfilters-savedqueries-apply-label": "建立查詢條件",
        "rcfilters-empty-filter": "沒有使用中的過濾條件。已顯示所有的貢獻。",
        "rcfilters-filterlist-title": "過濾條件",
        "rcfilters-filterlist-whatsthis": "這些是怎樣工作的?",
-       "rcfilters-filterlist-feedbacklink": "告訴我們您對這些(新)過濾工具有什麼想法",
+       "rcfilters-filterlist-feedbacklink": "告訴我們您對這些過濾工具有什麼想法",
        "rcfilters-highlightbutton-title": "明顯標示結果",
        "rcfilters-highlightmenu-title": "選擇顏色",
        "rcfilters-highlightmenu-help": "選擇要明顯標示此屬性的色彩",
diff --git a/maintenance/populateChangeTagDef.php b/maintenance/populateChangeTagDef.php
new file mode 100644 (file)
index 0000000..3b32c00
--- /dev/null
@@ -0,0 +1,192 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+require_once __DIR__ . '/Maintenance.php';
+
+/**
+ * Populate and improve accuracy of change_tag_def statistics.
+ *
+ * @ingroup Maintenance
+ */
+class PopulateChangeTagDef extends Maintenance {
+       /** @var Wikimedia\Rdbms\ILBFactory */
+       protected $lbFactory;
+
+       public function __construct() {
+               parent::__construct();
+               $this->addDescription( 'Populate and improve accuracy of change_tag_def statistics' );
+               $this->addOption( 'dry-run', 'Print debug info instead of actually deleting' );
+               $this->setBatchSize( 1000 );
+               $this->addOption(
+                       'sleep',
+                       'Sleep time (in seconds) between every batch',
+                       false,
+                       true
+               );
+       }
+
+       public function execute() {
+               global $wgChangeTagsSchemaMigrationStage;
+               $this->lbFactory = MediaWiki\MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+               $this->setBatchSize( $this->getOption( 'batch-size', $this->getBatchSize() ) );
+
+               $this->countDown( 5 );
+               if ( $wgChangeTagsSchemaMigrationStage < MIGRATION_NEW ) {
+                       $this->updateCountTag();
+                       $this->backpopulateChangeTagId();
+               } else {
+                       $this->updateCountTagId();
+               }
+
+               // TODO: Implement
+               // $this->cleanZeroCountRows();
+       }
+
+       private function updateCountTagId() {
+               $dbr = $this->lbFactory->getMainLB()->getConnection( DB_REPLICA );
+
+               // This query can be pretty expensive, don't run it on master
+               $res = $dbr->select(
+                       'change_tag',
+                       [ 'ct_tag_id', 'hitcount' => 'count(*)' ],
+                       [],
+                       __METHOD__,
+                       [ 'GROUP BY' => 'ct_tag_id' ]
+               );
+
+               $dbw = $this->lbFactory->getMainLB()->getConnection( DB_MASTER );
+
+               foreach ( $res as $row ) {
+                       if ( !$row->ct_tag_id ) {
+                               continue;
+                       }
+
+                       if ( $this->hasOption( 'dry-run' ) ) {
+                               $this->output( 'This row will be updated: ' . implode( ', ', $row ) . "\n" );
+                               continue;
+                       }
+
+                       $dbw->update(
+                               'change_tag_def',
+                               [ 'ctd_count' => $row->hitcount ],
+                               [ 'ctd_id' => $row->ct_tag_id ],
+                               __METHOD__
+                       );
+               }
+               $this->lbFactory->waitForReplication();
+       }
+
+       private function updateCountTag() {
+               $dbr = $this->lbFactory->getMainLB()->getConnection( DB_REPLICA );
+
+               // This query can be pretty expensive, don't run it on master
+               $res = $dbr->select(
+                       'change_tag',
+                       [ 'ct_tag', 'hitcount' => 'count(*)' ],
+                       [],
+                       __METHOD__,
+                       [ 'GROUP BY' => 'ct_tag' ]
+               );
+
+               $dbw = $this->lbFactory->getMainLB()->getConnection( DB_MASTER );
+
+               foreach ( $res as $row ) {
+                       // Hygiene check
+                       if ( !$row->ct_tag ) {
+                               continue;
+                       }
+
+                       if ( $this->hasOption( 'dry-run' ) ) {
+                               $this->output( 'This row will be updated: ' . $row->ct_tag . $row->hitcount . "\n" );
+                               continue;
+                       }
+
+                       $dbw->upsert(
+                               'change_tag_def',
+                               [
+                                       'ctd_name' => $row->ct_tag,
+                                       'ctd_user_defined' => 0,
+                                       'ctd_count' => $row->hitcount
+                               ],
+                               [ 'ctd_name' ],
+                               [ 'ctd_count' => $row->hitcount ],
+                               __METHOD__
+                       );
+               }
+               $this->lbFactory->waitForReplication();
+       }
+
+       private function backpopulateChangeTagId() {
+               $dbr = $this->lbFactory->getMainLB()->getConnection( DB_REPLICA );
+               $changeTagDefs = $dbr->select(
+                       'change_tag_def',
+                       [ 'ctd_name', 'ctd_id' ],
+                       [],
+                       __METHOD__
+               );
+
+               foreach ( $changeTagDefs as $row ) {
+                       $this->backpopulateChangeTagPerTag( $row->ctd_name, $row->ctd_id );
+               }
+       }
+
+       private function backpopulateChangeTagPerTag( $tagName, $tagId ) {
+               $dbr = $this->lbFactory->getMainLB()->getConnection( DB_REPLICA );
+               $dbw = $this->lbFactory->getMainLB()->getConnection( DB_MASTER );
+               $sleep = (int)$this->getOption( 'sleep', 10 );
+               $lastId = 0;
+               while ( true ) {
+                       // Given that indexes might not be there, it's better to use replica
+                       $ids = $dbr->selectFieldValues(
+                               'change_tag',
+                               'ct_id',
+                               [ 'ct_tag' => $tagName, 'ct_tag_id' => null, 'ct_id > ' . $lastId ],
+                               __METHOD__,
+                               [ 'LIMIT' => $this->getBatchSize() ]
+                       );
+
+                       if ( !$ids ) {
+                               break;
+                       }
+                       $lastId = end( $ids );
+
+                       if ( $this->hasOption( 'dry-run' ) ) {
+                               $this->output(
+                                       "These ids will be changed to have \"{$tagId}\" as tag id: " . implode( ', ', $ids ) . "\n"
+                               );
+                               continue;
+                       }
+
+                       $dbw->update(
+                               'change_tag',
+                               [ 'ct_tag_id' => $tagId ],
+                               [ 'ct_id' => $ids ],
+                               __METHOD__
+                       );
+
+                       $this->lbFactory->waitForReplication();
+                       if ( $sleep > 0 ) {
+                               sleep( $sleep );
+                       }
+               }
+       }
+
+}
+
+$maintClass = PopulateChangeTagDef::class;
+require_once RUN_MAINTENANCE_IF_MAIN;
index 49f1cd1..9ca4e98 100644 (file)
@@ -21,6 +21,7 @@
  * @ingroup Maintenance
  */
 
+use MediaWiki\MediaWikiServices;
 use Wikimedia\Rdbms\IDatabase;
 
 require_once __DIR__ . '/Maintenance.php';
@@ -258,7 +259,7 @@ class RefreshLinks extends Maintenance {
        public static function fixLinksFromArticle( $id, $ns = false ) {
                $page = WikiPage::newFromID( $id );
 
-               LinkCache::singleton()->clear();
+               MediaWikiServices::getInstance()->getLinkCache()->clear();
 
                if ( $page === null ) {
                        return;
index f99bb7d..fd7b860 100755 (executable)
@@ -38,9 +38,9 @@ fi
 
 # Copy file(s)
 rsync --force ./node_modules/oojs/dist/oojs.jquery.js "$REPO_DIR/$TARGET_DIR"
-rsync --force ./node_modules/oojs/dist/AUTHORS.txt "$REPO_DIR/$TARGET_DIR"
-rsync --force ./node_modules/oojs/dist/LICENSE-MIT "$REPO_DIR/$TARGET_DIR"
-rsync --force ./node_modules/oojs/dist/README.md "$REPO_DIR/$TARGET_DIR"
+rsync --force ./node_modules/oojs/AUTHORS.txt "$REPO_DIR/$TARGET_DIR"
+rsync --force ./node_modules/oojs/LICENSE-MIT "$REPO_DIR/$TARGET_DIR"
+rsync --force ./node_modules/oojs/README.md "$REPO_DIR/$TARGET_DIR"
 
 # Clean up temporary area
 rm -rf "$NPM_DIR"
index 36328e0..45533aa 100644 (file)
     "eslint": "4.9.0",
     "eslint-config-wikimedia": "0.5.0",
     "eslint-plugin-qunit": "3.2.1",
-    "grunt": "1.0.1",
+    "grunt": "1.0.3",
     "grunt-banana-checker": "0.6.0",
     "grunt-contrib-copy": "1.0.0",
-    "grunt-contrib-watch": "1.0.1",
+    "grunt-contrib-watch": "1.1.0",
     "grunt-eslint": "20.1.0",
     "grunt-jsonlint": "1.1.0",
     "grunt-karma": "2.0.0",
index 93ef4b1..69e9a2b 100644 (file)
@@ -321,17 +321,10 @@ return [
                'styles' => 'resources/src/jquery.tablesorter/jquery.tablesorter.less',
                'messages' => [ 'sort-descending', 'sort-ascending' ],
                'dependencies' => [
-                       'jquery.tablesorter.styles',
                        'mediawiki.RegExp',
                        'mediawiki.language.months',
                ],
        ],
-       'jquery.tablesorter.styles' => [
-               'targets' => [ 'desktop', 'mobile' ],
-               'styles' => [
-                       'resources/src/jquery/jquery.tablesorter.styles.less',
-               ],
-       ],
        'jquery.textSelection' => [
                'scripts' => 'resources/src/jquery/jquery.textSelection.js',
                'dependencies' => 'jquery.client',
@@ -1894,6 +1887,8 @@ return [
                'messages' => [
                        'rcfilters-tag-remove',
                        'rcfilters-activefilters',
+                       'rcfilters-activefilters-hide',
+                       'rcfilters-activefilters-show',
                        'rcfilters-advancedfilters',
                        'rcfilters-group-results-by-page',
                        'rcfilters-limit-title',
index ec14576..b99230a 100644 (file)
@@ -71,7 +71,7 @@ $ git remote update
 $ git checkout -B release -t origin/master
 
 # Ensure tests pass
-$ npm install && npm test
+$ npm install-test
 
 # Avoid using "npm version patch" because that creates
 # both a commit and a tag, and we shouldn't tag until after
index b51c5d1..3f400cd 100644 (file)
@@ -1,17 +1,18 @@
 /*!
- * OOjs v2.2.0 optimised for jQuery
+ * OOjs v2.2.2 optimised for jQuery
  * https://www.mediawiki.org/wiki/OOjs
  *
  * Copyright 2011-2018 OOjs Team and other contributors.
  * Released under the MIT license
  * https://oojs.mit-license.org
  *
- * Date: 2018-04-03T19:45:13Z
+ * Date: 2018-06-14T20:13:14Z
  */
 ( function ( global ) {
 
 'use strict';
 
+/* exported toString */
 var
        /**
         * Namespace for all classes, static methods and static properties.
@@ -21,8 +22,6 @@ var
        oo = {},
        // Optimisation: Local reference to Object.prototype.hasOwnProperty
        hasOwn = oo.hasOwnProperty,
-       // Marking this as "exported" doesn't work when parserOptions.sourceType is module
-       // eslint-disable-next-line no-unused-vars
        toString = oo.toString;
 
 /* Class Methods */
index 3bea471..ce24b0d 100644 (file)
@@ -8,10 +8,7 @@ table.jquery-tablesorter {
                cursor: pointer;
                background-repeat: no-repeat;
                background-position: center right;
-               // Note: To avoid reflows, a padding is set in
-               // the jquery.tableSorter.styles module as a render blocking style.
-               // Please do not add any CSS rules here that impact the positioning of the element
-               // e.g. padding, margin, position or float.
+               padding-right: 21px;
        }
 
        th.headerSortUp {
diff --git a/resources/src/jquery/jquery.tablesorter.styles.less b/resources/src/jquery/jquery.tablesorter.styles.less
deleted file mode 100644 (file)
index eda939d..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-.client-js {
-       // Reserve space for table sortable controls
-       table.sortable > thead > tr > th:not( .unsortable ) {
-               padding-right: 21px;
-       }
-}
index d81df65..4f9e43a 100644 (file)
         * e.g.
         *
         *        $.fn.msg = mediaWiki.jqueryMsg.getPlugin( options );
-        *        var userlink = $( '<a>' ).click( function () { alert( "hello!!" ) } );
-        *        $( 'p#headline' ).msg( 'hello-user', userlink );
+        *        var $userlink = $( '<a>' ).click( function () { alert( "hello!!" ) } );
+        *        $( 'p#headline' ).msg( 'hello-user', $userlink );
         *
         * N.B. replacements are variadic arguments or an array in second parameter. In other words:
         *    somefunction( a, b, c, d )
         * @return {jQuery}
         */
        mw.Message.prototype.parseDom = ( function () {
-               var reusableParent = $( '<div>' );
+               var $wrapper = $( '<div>' );
                return function () {
-                       return reusableParent.msg( this.key, this.parameters ).contents().detach();
+                       return $wrapper.msg( this.key, this.parameters ).contents().detach();
                };
        }() );
 
index 09d223e..11972de 100644 (file)
@@ -5,7 +5,7 @@
 @rcfilters-spinner-size: 12px;
 @rcfilters-head-min-height: 210px;
 @rcfilters-head-margin-bottom: 20px;
-@rcfilters-wl-head-min-height: 300px;
+@rcfilters-wl-head-min-height: 270px;
 
 // Corrections for the standard special page
 .client-js {
index 148e37b..83ca2bd 100644 (file)
                border-bottom: 0;
                background-color: @colorGray15;
                border-radius: 2px 2px 0 0;
-               padding: 0.6em;
+               padding: 0 0.6em 0.6em 0.6em;
                margin-top: 1em;
                line-height: normal;
        }
 
+       &-collapsed {
+               // Taking from the handle, since border-bottom is set on the
+               // filters view which is hidden when collapsed
+               border-bottom: 1px solid @colorGray10;
+
+               &.mw-rcfilters-ui-filterTagMultiselectWidget.oo-ui-widget-enabled .oo-ui-tagMultiselectWidget-handle {
+                       padding-bottom: 0;
+                       padding-top: 0;
+               }
+
+               .mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-hideshow {
+                       border-left: 1px solid @colorGray10;
+               }
+
+               &.oo-ui-tagMultiselectWidget-outlined {
+                       width: unset;
+                       max-width: 100%;
+               }
+
+               // Hide inner elements
+               .mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-filters,
+               .mw-rcfilters-ui-filterTagMultiselectWidget-views {
+                       display: none;
+               }
+       }
+
        .oo-ui-tagMultiselectWidget.oo-ui-widget-enabled &-animate.oo-ui-tagMultiselectWidget-handle {
                .transition( background-color 500ms ease-out );
        }
                background-color: @background-color-primary;
        }
 
+       &-hideshowButton.oo-ui-buttonElement > .oo-ui-buttonElement-button {
+               // Override the OOUI default for buttons
+               font-weight: normal;
+       }
+
        &-wrapper {
-               .mw-rcfilters-ui-table {
-                       margin-top: 0.3em;
+               &-top {
+                       display: flex;
+                       flex-wrap: nowrap;
+                       justify-content: space-between;
+               }
+
+               &-title {
+                       padding: 0.6em 0; // Same top padding as the handle
+                       white-space: nowrap;
+                       min-width: 0; // This has to be here to enable the text truncation
+                       overflow: hidden;
+                       text-overflow: ellipsis;
+               }
+
+               &-hideshow {
+                       margin-left: 0.5em;
+                       padding-left: 0.5em;
                }
 
                &-content {
                        &-title {
                                font-weight: bold;
                                color: @colorGray5;
+                               white-space: nowrap;
                        }
 
                        &-savedQueryTitle {
                                color: @colorGray2;
+                               padding-left: 1em;
                                font-weight: bold;
                                vertical-align: top;
-                               margin-left: 1em;
-                               width: ~'calc( 100% - 10em )';
                                overflow: hidden;
                                text-overflow: ellipsis;
                                white-space: nowrap;
index e72fbda..6746237 100644 (file)
@@ -8,7 +8,12 @@
        &-placeholder {
                &-title {
                        font-weight: bold;
-                       margin-bottom: 1em;
+                       margin-bottom: 0.4375em; // 7px / 16
+                       margin-top: 0.1875em; // 3px / 16
+               }
+
+               &-description {
+                       line-height: 1.5em; // 24px / 16
                }
 
                // Extra specificity needed to override OOUI rule that sets white-space: nowrap;
index 953df5b..5df7032 100644 (file)
                        classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-resetButton' ]
                } );
 
+               this.hideShowButton = new OO.ui.ButtonWidget( {
+                       framed: false,
+                       flags: [ 'progressive' ],
+                       label: mw.msg( 'rcfilters-activefilters-hide' ),
+                       classes: [ 'mw-rcfilters-ui-filterTagMultiselectWidget-hideshowButton' ]
+               } );
+               this.collapsed = false;
+
                if ( !mw.user.isAnon() ) {
                        this.saveQueryButton = new mw.rcfilters.ui.SaveFiltersPopupButtonWidget(
                                this.controller,
 
                // Events
                this.resetButton.connect( this, { click: 'onResetButtonClick' } );
+               this.hideShowButton.connect( this, { click: 'onHideShowButtonClick' } );
                // Stop propagation for mousedown, so that the widget doesn't
                // trigger the focus on the input and scrolls up when we click the reset button
                this.resetButton.$element.on( 'mousedown', function ( e ) { e.stopPropagation(); } );
+               this.hideShowButton.$element.on( 'mousedown', function ( e ) { e.stopPropagation(); } );
                this.model.connect( this, {
                        initialize: 'onModelInitialize',
                        update: 'onModelUpdate',
                                .append(
                                        $( '<div>' )
                                                .addClass( 'mw-rcfilters-ui-row' )
+                                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-views' )
                                                .append(
                                                        $( '<div>' )
                                                                .addClass( 'mw-rcfilters-ui-cell' )
 
                // Build the content
                $contentWrapper.append(
-                       title.$element,
-                       this.savedQueryTitle.$element,
                        $( '<div>' )
-                               .addClass( 'mw-rcfilters-ui-table' )
+                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-top' )
                                .append(
-                                       rcFiltersRow
-                               )
+                                       $( '<div>' )
+                                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-title' )
+                                               .append(
+                                                       title.$element,
+                                                       this.savedQueryTitle.$element
+                                               ),
+                                       $( '<div>' )
+                                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-hideshow' )
+                                               .append(
+                                                       this.hideShowButton.$element
+                                               )
+                               ),
+                       $( '<div>' )
+                               .addClass( 'mw-rcfilters-ui-table' )
+                               .addClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-wrapper-filters' )
+                               .append( rcFiltersRow )
                );
 
                // Initialize
         * @inheritdoc
         */
        mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onMouseDown = function ( e ) {
-               if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
+               if ( !this.collapsed && !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
                        this.menu.toggle();
 
                        return false;
                }
        };
 
+       /**
+        * Respond to hide/show button click
+        */
+       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.onHideShowButtonClick = function () {
+               this.toggleCollapsed();
+       };
+
+       /**
+        * Toggle the collapsed state of the filters widget
+        *
+        * @param {boolean} isCollapsed Widget is collapsed
+        */
+       mw.rcfilters.ui.FilterTagMultiselectWidget.prototype.toggleCollapsed = function ( isCollapsed ) {
+               isCollapsed = isCollapsed === undefined ? !this.collapsed : !!isCollapsed;
+
+               if ( this.collapsed !== isCollapsed ) {
+                       this.collapsed = isCollapsed;
+
+                       if ( isCollapsed ) {
+                               // If we are collapsing, close the menu, in case it was open
+                               // We should make sure the menu closes before the rest of the elements
+                               // are hidden, otherwise there is an unknown error in jQuery as ooui
+                               // sets and unsets properties on the input (which is hidden at that point)
+                               this.menu.toggle( false );
+                       }
+                       this.input.setDisabled( isCollapsed );
+                       this.hideShowButton.setLabel( mw.msg(
+                               isCollapsed ? 'rcfilters-activefilters-show' : 'rcfilters-activefilters-hide'
+                       ) );
+
+                       this.$element.toggleClass( 'mw-rcfilters-ui-filterTagMultiselectWidget-collapsed', isCollapsed );
+               }
+       };
+
        /**
         * Reevaluate the restore state for the widget between setting to defaults and clearing all filters
         */
index 060ead0..c13c2f7 100644 (file)
@@ -56,7 +56,7 @@
                                } ),
                                new OO.ui.MenuOptionWidget( {
                                        data: 'delete',
-                                       icon: 'close',
+                                       icon: 'trash',
                                        label: mw.msg( 'rcfilters-savedqueries-remove' )
                                } ),
                                new OO.ui.MenuOptionWidget( {
index f65a257..6558bda 100644 (file)
@@ -1,12 +1,38 @@
 ( function ( mw, $ ) {
+
+       // Return a promise that is resolved when the element is blurred (loses focus).
+       // If it already is blurred, resolved immediately.
+       function whenBlurred( $elem ) {
+               var deferred = $.Deferred();
+               if ( $elem.is( ':focus' ) ) {
+                       $elem.one( 'blur', deferred.resolve );
+               } else {
+                       deferred.resolve();
+               }
+               return deferred.promise();
+       }
+
        $( function () {
-               var startInput = mw.widgets.DateInputWidget.static.infuse( 'mw-date-start' ),
-                       endInput = mw.widgets.DateInputWidget.static.infuse( 'mw-date-end' );
+               var startReady, endReady;
+
+               // Do not infuse the date input while it has user focus.
+               // This is especially important on Firefox, where hiding the native date input while the native
+               // date picker is open will cause the date picker to remain visible (but non-functional), but
+               // not replacing the interface while the user is working with it is probably a good idea anyway.
+               startReady = whenBlurred( $( '#mw-date-start .oo-ui-inputWidget-input' ) ).then( function () {
+                       return mw.widgets.DateInputWidget.static.infuse( 'mw-date-start' );
+               } );
+               endReady = whenBlurred( $( '#mw-date-end .oo-ui-inputWidget-input' ) ).then( function () {
+                       return mw.widgets.DateInputWidget.static.infuse( 'mw-date-end' );
+               } );
 
-               startInput.on( 'deactivate', function ( userSelected ) {
-                       if ( userSelected ) {
-                               endInput.focus();
-                       }
+               $.when( startReady, endReady ).then( function ( startInput, endInput ) {
+                       startInput.on( 'deactivate', function ( userSelected ) {
+                               if ( userSelected ) {
+                                       endInput.focus();
+                               }
+                       } );
                } );
        } );
+
 }( mediaWiki, jQuery ) );
index bbfd528..4b8b1ee 100644 (file)
@@ -103,7 +103,6 @@ textarea.mw-ui-input {
 // <button class="mw-ui-button mw-ui-progressive">Submit</button>
 //
 // Styleguide 1.2.
-input[ type='number' ],
 .mw-ui-input-inline {
        display: inline-block;
        width: auto;
index 43ee202..9683570 100644 (file)
 ( function () {
        'use strict';
 
-       var slice = Array.prototype.slice;
+       var slice = Array.prototype.slice,
+               mwLoaderTrack = mw.track,
+               trackCallbacks = $.Callbacks( 'memory' ),
+               trackHandlers = [],
+               hasOwn = Object.prototype.hasOwnProperty;
 
        /**
         * Object constructor for messages.
        mw.msg = function () {
                return mw.message.apply( mw.message, arguments ).toString();
        };
+
+       /**
+        * Track an analytic event.
+        *
+        * This method provides a generic means for MediaWiki JavaScript code to capture state
+        * information for analysis. Each logged event specifies a string topic name that describes
+        * the kind of event that it is. Topic names consist of dot-separated path components,
+        * arranged from most general to most specific. Each path component should have a clear and
+        * well-defined purpose.
+        *
+        * Data handlers are registered via `mw.trackSubscribe`, and receive the full set of
+        * events that match their subcription, including those that fired before the handler was
+        * bound.
+        *
+        * @param {string} topic Topic name
+        * @param {Object} [data] Data describing the event, encoded as an object
+        */
+       mw.track = function ( topic, data ) {
+               mwLoaderTrack( topic, data );
+               trackCallbacks.fire( mw.trackQueue );
+       };
+
+       /**
+        * Register a handler for subset of analytic events, specified by topic.
+        *
+        * Handlers will be called once for each tracked event, including any events that fired before the
+        * handler was registered; 'this' is set to a plain object with a 'timeStamp' property indicating
+        * the exact time at which the event fired, a string 'topic' property naming the event, and a
+        * 'data' property which is an object of event-specific data. The event topic and event data are
+        * also passed to the callback as the first and second arguments, respectively.
+        *
+        * @param {string} topic Handle events whose name starts with this string prefix
+        * @param {Function} callback Handler to call for each matching tracked event
+        * @param {string} callback.topic
+        * @param {Object} [callback.data]
+        */
+       mw.trackSubscribe = function ( topic, callback ) {
+               var seen = 0;
+               function handler( trackQueue ) {
+                       var event;
+                       for ( ; seen < trackQueue.length; seen++ ) {
+                               event = trackQueue[ seen ];
+                               if ( event.topic.indexOf( topic ) === 0 ) {
+                                       callback.call( event, event.topic, event.data );
+                               }
+                       }
+               }
+
+               trackHandlers.push( [ handler, callback ] );
+
+               trackCallbacks.add( handler );
+       };
+
+       /**
+        * Stop handling events for a particular handler
+        *
+        * @param {Function} callback
+        */
+       mw.trackUnsubscribe = function ( callback ) {
+               trackHandlers = trackHandlers.filter( function ( fns ) {
+                       if ( fns[ 1 ] === callback ) {
+                               trackCallbacks.remove( fns[ 0 ] );
+                               // Ensure the tuple is removed to avoid holding on to closures
+                               return false;
+                       }
+                       return true;
+               } );
+       };
+
+       // Fire events from before track() triggred fire()
+       trackCallbacks.fire( mw.trackQueue );
+
+       /**
+        * Registry and firing of events.
+        *
+        * MediaWiki has various interface components that are extended, enhanced
+        * or manipulated in some other way by extensions, gadgets and even
+        * in core itself.
+        *
+        * This framework helps streamlining the timing of when these other
+        * code paths fire their plugins (instead of using document-ready,
+        * which can and should be limited to firing only once).
+        *
+        * Features like navigating to other wiki pages, previewing an edit
+        * and editing itself – without a refresh – can then retrigger these
+        * hooks accordingly to ensure everything still works as expected.
+        *
+        * Example usage:
+        *
+        *     mw.hook( 'wikipage.content' ).add( fn ).remove( fn );
+        *     mw.hook( 'wikipage.content' ).fire( $content );
+        *
+        * Handlers can be added and fired for arbitrary event names at any time. The same
+        * event can be fired multiple times. The last run of an event is memorized
+        * (similar to `$(document).ready` and `$.Deferred().done`).
+        * This means if an event is fired, and a handler added afterwards, the added
+        * function will be fired right away with the last given event data.
+        *
+        * Like Deferreds and Promises, the mw.hook object is both detachable and chainable.
+        * Thus allowing flexible use and optimal maintainability and authority control.
+        * You can pass around the `add` and/or `fire` method to another piece of code
+        * without it having to know the event name (or `mw.hook` for that matter).
+        *
+        *     var h = mw.hook( 'bar.ready' );
+        *     new mw.Foo( .. ).fetch( { callback: h.fire } );
+        *
+        * Note: Events are documented with an underscore instead of a dot in the event
+        * name due to jsduck not supporting dots in that position.
+        *
+        * @class mw.hook
+        */
+       mw.hook = ( function () {
+               var lists = {};
+
+               /**
+                * Create an instance of mw.hook.
+                *
+                * @method hook
+                * @member mw
+                * @param {string} name Name of hook.
+                * @return {mw.hook}
+                */
+               return function ( name ) {
+                       var list = hasOwn.call( lists, name ) ?
+                               lists[ name ] :
+                               lists[ name ] = $.Callbacks( 'memory' );
+
+                       return {
+                               /**
+                                * Register a hook handler
+                                *
+                                * @param {...Function} handler Function to bind.
+                                * @chainable
+                                */
+                               add: list.add,
+
+                               /**
+                                * Unregister a hook handler
+                                *
+                                * @param {...Function} handler Function to unbind.
+                                * @chainable
+                                */
+                               remove: list.remove,
+
+                               /**
+                                * Run a hook.
+                                *
+                                * @param {...Mixed} data
+                                * @return {mw.hook}
+                                * @chainable
+                                */
+                               fire: function () {
+                                       return list.fireWith.call( this, null, slice.call( arguments ) );
+                               }
+                       };
+               };
+       }() );
+
+       /**
+        * HTML construction helper functions
+        *
+        *     @example
+        *
+        *     var Html, output;
+        *
+        *     Html = mw.html;
+        *     output = Html.element( 'div', {}, new Html.Raw(
+        *         Html.element( 'img', { src: '<' } )
+        *     ) );
+        *     mw.log( output ); // <div><img src="&lt;"/></div>
+        *
+        * @class mw.html
+        * @singleton
+        */
+       mw.html = ( function () {
+               function escapeCallback( s ) {
+                       switch ( s ) {
+                               case '\'':
+                                       return '&#039;';
+                               case '"':
+                                       return '&quot;';
+                               case '<':
+                                       return '&lt;';
+                               case '>':
+                                       return '&gt;';
+                               case '&':
+                                       return '&amp;';
+                       }
+               }
+
+               return {
+                       /**
+                        * Escape a string for HTML.
+                        *
+                        * Converts special characters to HTML entities.
+                        *
+                        *     mw.html.escape( '< > \' & "' );
+                        *     // Returns &lt; &gt; &#039; &amp; &quot;
+                        *
+                        * @param {string} s The string to escape
+                        * @return {string} HTML
+                        */
+                       escape: function ( s ) {
+                               return s.replace( /['"<>&]/g, escapeCallback );
+                       },
+
+                       /**
+                        * Create an HTML element string, with safe escaping.
+                        *
+                        * @param {string} name The tag name.
+                        * @param {Object} [attrs] An object with members mapping element names to values
+                        * @param {string|mw.html.Raw|mw.html.Cdata|null} [contents=null] The contents of the element.
+                        *
+                        *  - string: Text to be escaped.
+                        *  - null: The element is treated as void with short closing form, e.g. `<br/>`.
+                        *  - this.Raw: The raw value is directly included.
+                        *  - this.Cdata: The raw value is directly included. An exception is
+                        *    thrown if it contains any illegal ETAGO delimiter.
+                        *    See <https://www.w3.org/TR/html401/appendix/notes.html#h-B.3.2>.
+                        * @return {string} HTML
+                        */
+                       element: function ( name, attrs, contents ) {
+                               var v, attrName, s = '<' + name;
+
+                               if ( attrs ) {
+                                       for ( attrName in attrs ) {
+                                               v = attrs[ attrName ];
+                                               // Convert name=true, to name=name
+                                               if ( v === true ) {
+                                                       v = attrName;
+                                                       // Skip name=false
+                                               } else if ( v === false ) {
+                                                       continue;
+                                               }
+                                               s += ' ' + attrName + '="' + this.escape( String( v ) ) + '"';
+                                       }
+                               }
+                               if ( contents === undefined || contents === null ) {
+                                       // Self close tag
+                                       s += '/>';
+                                       return s;
+                               }
+                               // Regular open tag
+                               s += '>';
+                               switch ( typeof contents ) {
+                                       case 'string':
+                                               // Escaped
+                                               s += this.escape( contents );
+                                               break;
+                                       case 'number':
+                                       case 'boolean':
+                                               // Convert to string
+                                               s += String( contents );
+                                               break;
+                                       default:
+                                               if ( contents instanceof this.Raw ) {
+                                                       // Raw HTML inclusion
+                                                       s += contents.value;
+                                               } else if ( contents instanceof this.Cdata ) {
+                                                       // CDATA
+                                                       if ( /<\/[a-zA-z]/.test( contents.value ) ) {
+                                                               throw new Error( 'mw.html.element: Illegal end tag found in CDATA' );
+                                                       }
+                                                       s += contents.value;
+                                               } else {
+                                                       throw new Error( 'mw.html.element: Invalid type of contents' );
+                                               }
+                               }
+                               s += '</' + name + '>';
+                               return s;
+                       },
+
+                       /**
+                        * Wrapper object for raw HTML passed to mw.html.element().
+                        *
+                        * @class mw.html.Raw
+                        * @constructor
+                        * @param {string} value
+                        */
+                       Raw: function ( value ) {
+                               this.value = value;
+                       },
+
+                       /**
+                        * Wrapper object for CDATA element contents passed to mw.html.element()
+                        *
+                        * @class mw.html.Cdata
+                        * @constructor
+                        * @param {string} value
+                        */
+                       Cdata: function ( value ) {
+                               this.value = value;
+                       }
+               };
+       }() );
 }() );
index 9d34bf1..7d40ce7 100644 (file)
@@ -13,9 +13,6 @@
 
        var mw, StringSet, log,
                hasOwn = Object.prototype.hasOwnProperty,
-               slice = Array.prototype.slice,
-               trackCallbacks = $.Callbacks( 'memory' ),
-               trackHandlers = [],
                trackQueue = [];
 
        /**
                );
        }
 
+       /**
+        * Log a message to window.console, if possible.
+        *
+        * Useful to force logging of some errors that are otherwise hard to detect (i.e., this logs
+        * also in production mode). Gets console references in each invocation instead of caching the
+        * reference, so that debugging tools loaded later are supported (e.g. Firebug Lite in IE).
+        *
+        * @private
+        * @param {string} topic Stream name passed by mw.track
+        * @param {Object} data Data passed by mw.track
+        * @param {Error} [data.exception]
+        * @param {string} data.source Error source
+        * @param {string} [data.module] Name of module which caused the error
+        */
+       function logError( topic, data ) {
+               /* eslint-disable no-console */
+               var msg,
+                       e = data.exception,
+                       source = data.source,
+                       module = data.module,
+                       console = window.console;
+
+               if ( console && console.log ) {
+                       msg = ( e ? 'Exception' : 'Error' ) + ' in ' + source;
+                       if ( module ) {
+                               msg += ' in module ' + module;
+                       }
+                       msg += ( e ? ':' : '.' );
+                       console.log( msg );
+
+                       // If we have an exception object, log it to the warning channel to trigger
+                       // proper stacktraces in browsers that support it.
+                       if ( e && console.warn ) {
+                               console.warn( e );
+                       }
+               }
+               /* eslint-enable no-console */
+       }
+
        /**
         * Create an object that can be read from or written to via methods that allow
         * interaction both with single and multiple properties at once.
                }() ),
 
                /**
-                * Track an analytic event.
-                *
-                * This method provides a generic means for MediaWiki JavaScript code to capture state
-                * information for analysis. Each logged event specifies a string topic name that describes
-                * the kind of event that it is. Topic names consist of dot-separated path components,
-                * arranged from most general to most specific. Each path component should have a clear and
-                * well-defined purpose.
+                * List of all analytic events emitted so far.
                 *
-                * Data handlers are registered via `mw.trackSubscribe`, and receive the full set of
-                * events that match their subcription, including those that fired before the handler was
-                * bound.
-                *
-                * @param {string} topic Topic name
-                * @param {Object} [data] Data describing the event, encoded as an object
+                * @private
+                * @property {Array}
                 */
+               trackQueue: trackQueue,
+
                track: function ( topic, data ) {
                        trackQueue.push( { topic: topic, timeStamp: mw.now(), data: data } );
-                       trackCallbacks.fire( trackQueue );
-               },
-
-               /**
-                * Register a handler for subset of analytic events, specified by topic.
-                *
-                * Handlers will be called once for each tracked event, including any events that fired before the
-                * handler was registered; 'this' is set to a plain object with a 'timeStamp' property indicating
-                * the exact time at which the event fired, a string 'topic' property naming the event, and a
-                * 'data' property which is an object of event-specific data. The event topic and event data are
-                * also passed to the callback as the first and second arguments, respectively.
-                *
-                * @param {string} topic Handle events whose name starts with this string prefix
-                * @param {Function} callback Handler to call for each matching tracked event
-                * @param {string} callback.topic
-                * @param {Object} [callback.data]
-                */
-               trackSubscribe: function ( topic, callback ) {
-                       var seen = 0;
-                       function handler( trackQueue ) {
-                               var event;
-                               for ( ; seen < trackQueue.length; seen++ ) {
-                                       event = trackQueue[ seen ];
-                                       if ( event.topic.indexOf( topic ) === 0 ) {
-                                               callback.call( event, event.topic, event.data );
-                                       }
-                               }
-                       }
-
-                       trackHandlers.push( [ handler, callback ] );
-
-                       trackCallbacks.add( handler );
+                       // The base module extends this method to fire events here
                },
 
                /**
-                * Stop handling events for a particular handler
+                * Track an early error event via mw.track and send it to the window console.
                 *
-                * @param {Function} callback
+                * @private
+                * @param {string} topic Topic name
+                * @param {Object} data Data describing the event, encoded as an object; see mw#logError
                 */
-               trackUnsubscribe: function ( callback ) {
-                       trackHandlers = trackHandlers.filter( function ( fns ) {
-                               if ( fns[ 1 ] === callback ) {
-                                       trackCallbacks.remove( fns[ 0 ] );
-                                       // Ensure the tuple is removed to avoid holding on to closures
-                                       return false;
-                               }
-                               return true;
-                       } );
+               trackError: function ( topic, data ) {
+                       mw.track( topic, data );
+                       logError( topic, data );
                },
 
                // Expose Map constructor
                                                } catch ( e ) {
                                                        // A user-defined callback raised an exception.
                                                        // Swallow it to protect our state machine!
-                                                       mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'load-callback' } );
+                                                       mw.trackError( 'resourceloader.exception', {
+                                                               exception: e,
+                                                               module: module,
+                                                               source: 'load-callback'
+                                                       } );
                                                }
                                        }
                                }
                                                // This module is unknown or has unknown dependencies.
                                                // Undo any incomplete resolutions made and keep going.
                                                resolved = saved;
-                                               mw.track( 'resourceloader.exception', {
+                                               mw.trackError( 'resourceloader.exception', {
                                                        exception: err,
                                                        source: 'resolve'
                                                } );
                                                } else if ( typeof script === 'function' ) {
                                                        // Pass jQuery twice so that the signature of the closure which wraps
                                                        // the script can bind both '$' and 'jQuery'.
-                                                       script( $, $, mw.loader.require, registry[ module ].module );
+                                                       script( window.$, window.$, mw.loader.require, registry[ module ].module );
                                                        markModuleReady();
 
                                                } else if ( typeof script === 'string' ) {
                                                // Use mw.track instead of mw.log because these errors are common in production mode
                                                // (e.g. undefined variable), and mw.log is only enabled in debug mode.
                                                registry[ module ].state = 'error';
-                                               mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'module-execute' } );
+                                               mw.trackError( 'resourceloader.exception', {
+                                                       exception: e, module:
+                                                       module, source: 'module-execute'
+                                               } );
                                                handlePending( module );
                                        }
                                };
                                                        // risks and clear everything in this cache.
                                                        mw.loader.store.clear();
 
-                                                       mw.track( 'resourceloader.exception', { exception: err, source: 'store-eval' } );
+                                                       mw.trackError( 'resourceloader.exception', {
+                                                               exception: err,
+                                                               source: 'store-eval'
+                                                       } );
                                                        // Re-add the failed ones that are still pending back to the batch
                                                        failed = sourceModules.filter( function ( module ) {
                                                                return registry[ module ].state === 'loading';
                                                                return;
                                                        }
                                                } catch ( e ) {
-                                                       mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-init' } );
+                                                       mw.trackError( 'resourceloader.exception', {
+                                                               exception: e,
+                                                               source: 'store-localstorage-init'
+                                                       } );
                                                }
 
                                                if ( raw === undefined ) {
                                                        // This regex should never match under sane conditions.
                                                        if ( /^\s*\(/.test( args[ 1 ] ) ) {
                                                                args[ 1 ] = 'function' + args[ 1 ];
-                                                               mw.track( 'resourceloader.assert', { source: 'bug-T59567' } );
+                                                               mw.trackError( 'resourceloader.assert', { source: 'bug-T59567' } );
                                                        }
                                                } catch ( e ) {
-                                                       mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-json' } );
+                                                       mw.trackError( 'resourceloader.exception', {
+                                                               exception: e,
+                                                               source: 'store-localstorage-json'
+                                                       } );
                                                        return false;
                                                }
 
                                                                data = JSON.stringify( mw.loader.store );
                                                                localStorage.setItem( key, data );
                                                        } catch ( e ) {
-                                                               mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-update' } );
+                                                               mw.trackError( 'resourceloader.exception', {
+                                                                       exception: e,
+                                                                       source: 'store-localstorage-update'
+                                                               } );
                                                        }
 
                                                        hasPendingWrite = false;
                        };
                }() ),
 
-               /**
-                * HTML construction helper functions
-                *
-                *     @example
-                *
-                *     var Html, output;
-                *
-                *     Html = mw.html;
-                *     output = Html.element( 'div', {}, new Html.Raw(
-                *         Html.element( 'img', { src: '<' } )
-                *     ) );
-                *     mw.log( output ); // <div><img src="&lt;"/></div>
-                *
-                * @class mw.html
-                * @singleton
-                */
-               html: ( function () {
-                       function escapeCallback( s ) {
-                               switch ( s ) {
-                                       case '\'':
-                                               return '&#039;';
-                                       case '"':
-                                               return '&quot;';
-                                       case '<':
-                                               return '&lt;';
-                                       case '>':
-                                               return '&gt;';
-                                       case '&':
-                                               return '&amp;';
-                               }
-                       }
-
-                       return {
-                               /**
-                                * Escape a string for HTML.
-                                *
-                                * Converts special characters to HTML entities.
-                                *
-                                *     mw.html.escape( '< > \' & "' );
-                                *     // Returns &lt; &gt; &#039; &amp; &quot;
-                                *
-                                * @param {string} s The string to escape
-                                * @return {string} HTML
-                                */
-                               escape: function ( s ) {
-                                       return s.replace( /['"<>&]/g, escapeCallback );
-                               },
-
-                               /**
-                                * Create an HTML element string, with safe escaping.
-                                *
-                                * @param {string} name The tag name.
-                                * @param {Object} [attrs] An object with members mapping element names to values
-                                * @param {string|mw.html.Raw|mw.html.Cdata|null} [contents=null] The contents of the element.
-                                *
-                                *  - string: Text to be escaped.
-                                *  - null: The element is treated as void with short closing form, e.g. `<br/>`.
-                                *  - this.Raw: The raw value is directly included.
-                                *  - this.Cdata: The raw value is directly included. An exception is
-                                *    thrown if it contains any illegal ETAGO delimiter.
-                                *    See <https://www.w3.org/TR/html401/appendix/notes.html#h-B.3.2>.
-                                * @return {string} HTML
-                                */
-                               element: function ( name, attrs, contents ) {
-                                       var v, attrName, s = '<' + name;
-
-                                       if ( attrs ) {
-                                               for ( attrName in attrs ) {
-                                                       v = attrs[ attrName ];
-                                                       // Convert name=true, to name=name
-                                                       if ( v === true ) {
-                                                               v = attrName;
-                                                       // Skip name=false
-                                                       } else if ( v === false ) {
-                                                               continue;
-                                                       }
-                                                       s += ' ' + attrName + '="' + this.escape( String( v ) ) + '"';
-                                               }
-                                       }
-                                       if ( contents === undefined || contents === null ) {
-                                               // Self close tag
-                                               s += '/>';
-                                               return s;
-                                       }
-                                       // Regular open tag
-                                       s += '>';
-                                       switch ( typeof contents ) {
-                                               case 'string':
-                                                       // Escaped
-                                                       s += this.escape( contents );
-                                                       break;
-                                               case 'number':
-                                               case 'boolean':
-                                                       // Convert to string
-                                                       s += String( contents );
-                                                       break;
-                                               default:
-                                                       if ( contents instanceof this.Raw ) {
-                                                               // Raw HTML inclusion
-                                                               s += contents.value;
-                                                       } else if ( contents instanceof this.Cdata ) {
-                                                               // CDATA
-                                                               if ( /<\/[a-zA-z]/.test( contents.value ) ) {
-                                                                       throw new Error( 'mw.html.element: Illegal end tag found in CDATA' );
-                                                               }
-                                                               s += contents.value;
-                                                       } else {
-                                                               throw new Error( 'mw.html.element: Invalid type of contents' );
-                                                       }
-                                       }
-                                       s += '</' + name + '>';
-                                       return s;
-                               },
-
-                               /**
-                                * Wrapper object for raw HTML passed to mw.html.element().
-                                *
-                                * @class mw.html.Raw
-                                * @constructor
-                                * @param {string} value
-                                */
-                               Raw: function ( value ) {
-                                       this.value = value;
-                               },
-
-                               /**
-                                * Wrapper object for CDATA element contents passed to mw.html.element()
-                                *
-                                * @class mw.html.Cdata
-                                * @constructor
-                                * @param {string} value
-                                */
-                               Cdata: function ( value ) {
-                                       this.value = value;
-                               }
-                       };
-               }() ),
-
                // Skeleton user object, extended by the 'mediawiki.user' module.
                /**
                 * @class mw.user
                },
 
                // OOUI widgets specific to MediaWiki
-               widgets: {},
+               widgets: {}
 
-               /**
-                * Registry and firing of events.
-                *
-                * MediaWiki has various interface components that are extended, enhanced
-                * or manipulated in some other way by extensions, gadgets and even
-                * in core itself.
-                *
-                * This framework helps streamlining the timing of when these other
-                * code paths fire their plugins (instead of using document-ready,
-                * which can and should be limited to firing only once).
-                *
-                * Features like navigating to other wiki pages, previewing an edit
-                * and editing itself – without a refresh – can then retrigger these
-                * hooks accordingly to ensure everything still works as expected.
-                *
-                * Example usage:
-                *
-                *     mw.hook( 'wikipage.content' ).add( fn ).remove( fn );
-                *     mw.hook( 'wikipage.content' ).fire( $content );
-                *
-                * Handlers can be added and fired for arbitrary event names at any time. The same
-                * event can be fired multiple times. The last run of an event is memorized
-                * (similar to `$(document).ready` and `$.Deferred().done`).
-                * This means if an event is fired, and a handler added afterwards, the added
-                * function will be fired right away with the last given event data.
-                *
-                * Like Deferreds and Promises, the mw.hook object is both detachable and chainable.
-                * Thus allowing flexible use and optimal maintainability and authority control.
-                * You can pass around the `add` and/or `fire` method to another piece of code
-                * without it having to know the event name (or `mw.hook` for that matter).
-                *
-                *     var h = mw.hook( 'bar.ready' );
-                *     new mw.Foo( .. ).fetch( { callback: h.fire } );
-                *
-                * Note: Events are documented with an underscore instead of a dot in the event
-                * name due to jsduck not supporting dots in that position.
-                *
-                * @class mw.hook
-                */
-               hook: ( function () {
-                       var lists = {};
-
-                       /**
-                        * Create an instance of mw.hook.
-                        *
-                        * @method hook
-                        * @member mw
-                        * @param {string} name Name of hook.
-                        * @return {mw.hook}
-                        */
-                       return function ( name ) {
-                               var list = hasOwn.call( lists, name ) ?
-                                       lists[ name ] :
-                                       lists[ name ] = $.Callbacks( 'memory' );
-
-                               return {
-                                       /**
-                                        * Register a hook handler
-                                        *
-                                        * @param {...Function} handler Function to bind.
-                                        * @chainable
-                                        */
-                                       add: list.add,
-
-                                       /**
-                                        * Unregister a hook handler
-                                        *
-                                        * @param {...Function} handler Function to unbind.
-                                        * @chainable
-                                        */
-                                       remove: list.remove,
-
-                                       /**
-                                        * Run a hook.
-                                        *
-                                        * @param {...Mixed} data
-                                        * @return {mw.hook}
-                                        * @chainable
-                                        */
-                                       fire: function () {
-                                               return list.fireWith.call( this, null, slice.call( arguments ) );
-                                       }
-                               };
-                       };
-               }() )
        };
 
        // Alias $j to jQuery for backwards compatibility
        // @deprecated since 1.23 Use $ or jQuery instead
        mw.log.deprecate( window, '$j', $, 'Use $ or jQuery instead.' );
 
-       /**
-        * Log a message to window.console, if possible.
-        *
-        * Useful to force logging of some errors that are otherwise hard to detect (i.e., this logs
-        * also in production mode). Gets console references in each invocation instead of caching the
-        * reference, so that debugging tools loaded later are supported (e.g. Firebug Lite in IE).
-        *
-        * @private
-        * @param {string} topic Stream name passed by mw.track
-        * @param {Object} data Data passed by mw.track
-        * @param {Error} [data.exception]
-        * @param {string} data.source Error source
-        * @param {string} [data.module] Name of module which caused the error
-        */
-       function logError( topic, data ) {
-               /* eslint-disable no-console */
-               var msg,
-                       e = data.exception,
-                       source = data.source,
-                       module = data.module,
-                       console = window.console;
-
-               if ( console && console.log ) {
-                       msg = ( e ? 'Exception' : 'Error' ) + ' in ' + source;
-                       if ( module ) {
-                               msg += ' in module ' + module;
-                       }
-                       msg += ( e ? ':' : '.' );
-                       console.log( msg );
-
-                       // If we have an exception object, log it to the warning channel to trigger
-                       // proper stacktraces in browsers that support it.
-                       if ( e && console.warn ) {
-                               console.warn( e );
-                       }
-               }
-               /* eslint-enable no-console */
-       }
-
-       // Subscribe to error streams
-       mw.trackSubscribe( 'resourceloader.exception', logError );
-       mw.trackSubscribe( 'resourceloader.assert', logError );
-
        // Attach to window and globally alias
        window.mw = window.mediaWiki = mw;
 }( jQuery ) );
index a798679..88c541e 100644 (file)
@@ -150,6 +150,8 @@ $wgAutoloadClasses += [
 
        # tests/phpunit/includes/Storage
        'MediaWiki\Tests\Storage\McrSchemaDetection' => "$testDir/phpunit/includes/Storage/McrSchemaDetection.php",
+       'MediaWiki\Tests\Storage\McrSchemaOverride' => "$testDir/phpunit/includes/Storage/McrSchemaOverride.php",
+       'MediaWiki\Tests\Storage\McrWriteBothSchemaOverride' => "$testDir/phpunit/includes/Storage/McrWriteBothSchemaOverride.php",
        'MediaWiki\Tests\Storage\RevisionSlotsTest' => "$testDir/phpunit/includes/Storage/RevisionSlotsTest.php",
        'MediaWiki\Tests\Storage\RevisionRecordTests' => "$testDir/phpunit/includes/Storage/RevisionRecordTests.php",
        'MediaWiki\Tests\Storage\RevisionStoreDbTestBase' => "$testDir/phpunit/includes/Storage/RevisionStoreDbTestBase.php",
@@ -184,6 +186,10 @@ $wgAutoloadClasses += [
                => "$testDir/phpunit/mocks/session/DummySessionBackend.php",
        'DummySessionProvider' => "$testDir/phpunit/mocks/session/DummySessionProvider.php",
        'MockMessageLocalizer' => "$testDir/phpunit/mocks/MockMessageLocalizer.php",
+       'MockCompletionSearchEngine' => "$testDir/phpunit/mocks/search/MockCompletionSearchEngine.php",
+       'MockSearchEngine' => "$testDir/phpunit/mocks/search/MockSearchEngine.php",
+       'MockSearchResultSet' => "$testDir/phpunit/mocks/search/MockSearchResultSet.php",
+       'MockSearchResult' => "$testDir/phpunit/mocks/search/MockSearchResult.php",
 
        # tests/suites
        'ParserTestFileSuite' => "$testDir/phpunit/suites/ParserTestFileSuite.php",
index 97175da..60a5881 100644 (file)
@@ -1281,7 +1281,7 @@ class ParserTestRunner {
 
                // Wipe some DB query result caches on setup and teardown
                $reset = function () {
-                       LinkCache::singleton()->clear();
+                       MediaWikiServices::getInstance()->getLinkCache()->clear();
 
                        // Clear the message cache
                        MessageCache::singleton()->clear();
index 6bcbd93..4140c23 100644 (file)
@@ -178,6 +178,55 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase {
                return self::getTestUser( [ 'sysop', 'bureaucrat' ] );
        }
 
+       /**
+        * Returns a WikiPage representing an existing page.
+        *
+        * @since 1.32
+        *
+        * @param Title|string|null $title
+        * @return WikiPage
+        * @throws MWException
+        */
+       protected function getExistingTestPage( $title = null ) {
+               $title = ( $title === null ) ? 'UTPage' : $title;
+               $title = is_string( $title ) ? Title::newFromText( $title ) : $title;
+               $page = WikiPage::factory( $title );
+
+               if ( !$page->exists() ) {
+                       $user = self::getTestSysop()->getUser();
+                       $page->doEditContent(
+                               new WikitextContent( 'UTContent' ),
+                               'UTPageSummary',
+                               EDIT_NEW | EDIT_SUPPRESS_RC,
+                               false,
+                               $user
+                       );
+               }
+
+               return $page;
+       }
+
+       /**
+        * Returns a WikiPage representing a non-existing page.
+        *
+        * @since 1.32
+        *
+        * @param Title|string|null $title
+        * @return WikiPage
+        * @throws MWException
+        */
+       protected function getNonexistingTestPage( $title = null ) {
+               $title = ( $title === null ) ? 'UTPage-' . rand( 0, 100000 ) : $title;
+               $title = is_string( $title ) ? Title::newFromText( $title ) : $title;
+               $page = WikiPage::factory( $title );
+
+               if ( $page->exists() ) {
+                       $page->doDeleteArticle( 'Testing' );
+               }
+
+               return $page;
+       }
+
        /**
         * Prepare service configuration for unit testing.
         *
@@ -1023,7 +1072,7 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase {
         * Should be called from addDBData().
         *
         * @since 1.25 ($namespace in 1.28)
-        * @param string|title $pageName Page name or title
+        * @param string|Title $pageName Page name or title
         * @param string $text Page's content
         * @param int $namespace Namespace id (name cannot already contain namespace)
         * @return array Title object and page id
@@ -1081,7 +1130,10 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase {
        public function addDBData() {
        }
 
-       private function addCoreDBData() {
+       /**
+        * @since 1.32
+        */
+       protected function addCoreDBData() {
                if ( $this->db->getType() == 'oracle' ) {
                        # Insert 0 user to prevent FK violations
                        # Anonymous user
@@ -1335,8 +1387,10 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase {
        /**
         * @throws LogicException if the given database connection is not a set up to use
         * mock tables.
+        *
+        * @since 1.31 this is no longer private.
         */
-       private function ensureMockDatabaseConnection( IDatabase $db ) {
+       protected function ensureMockDatabaseConnection( IDatabase $db ) {
                if ( $db->tablePrefix() !== $this->dbPrefix() ) {
                        throw new LogicException(
                                'Trying to delete mock tables, but table prefix does not indicate a mock database.'
@@ -1486,7 +1540,7 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase {
                        if ( $tbl === 'page' ) {
                                // Forget about the pages since they don't
                                // exist in the DB.
-                               LinkCache::singleton()->clear();
+                               MediaWikiServices::getInstance()->getLinkCache()->clear();
                        }
                }
        }
@@ -1550,8 +1604,10 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase {
        private function resetDB( $db, $tablesUsed ) {
                if ( $db ) {
                        $userTables = [ 'user', 'user_groups', 'user_properties', 'actor' ];
-                       $pageTables = [ 'page', 'revision', 'ip_changes', 'revision_comment_temp',
-                               'revision_actor_temp', 'comment', 'archive' ];
+                       $pageTables = [
+                               'page', 'revision', 'ip_changes', 'revision_comment_temp', 'comment', 'archive',
+                               'revision_actor_temp', 'slots', 'content', 'content_models', 'slot_roles',
+                       ];
                        $coreDBDataTables = array_merge( $userTables, $pageTables );
 
                        // If any of the user or page tables were marked as used, we should clear all of them.
@@ -1601,7 +1657,7 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase {
                                if ( $tbl === 'page' ) {
                                        // Forget about the pages since they don't
                                        // exist in the DB.
-                                       LinkCache::singleton()->clear();
+                                       MediaWikiServices::getInstance()->getLinkCache()->clear();
                                }
                        }
 
index f9e2cc1..4c508e3 100644 (file)
@@ -1,6 +1,5 @@
 <?php
 
-use MediaWiki\MediaWikiServices;
 
 /**
  * @group Database
@@ -370,6 +369,7 @@ class LinkerTest extends MediaWikiLangTestCase {
         * @dataProvider provideLinkBeginHook
         */
        public function testLinkBeginHook( $callback, $expected ) {
+               $this->hideDeprecated( 'LinkBegin hook (used in hook-LinkBegin-closure)' );
                $this->setMwGlobals( [
                        'wgArticlePath' => '/wiki/$1',
                        'wgServer' => '//example.org',
@@ -417,6 +417,7 @@ class LinkerTest extends MediaWikiLangTestCase {
         * @dataProvider provideLinkEndHook
         */
        public function testLinkEndHook( $callback, $expected ) {
+               $this->hideDeprecated( 'LinkEnd hook (used in hook-LinkEnd-closure)' );
                $this->setMwGlobals( [
                        'wgArticlePath' => '/wiki/$1',
                ] );
@@ -427,54 +428,4 @@ class LinkerTest extends MediaWikiLangTestCase {
                $out = Linker::link( $title );
                $this->assertEquals( $expected, $out );
        }
-
-       /**
-        * @covers Linker::getLinkColour
-        */
-       public function testGetLinkColour() {
-               $this->hideDeprecated( 'Linker::getLinkColour' );
-               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
-               $foobarTitle = Title::makeTitle( NS_MAIN, 'FooBar' );
-               $redirectTitle = Title::makeTitle( NS_MAIN, 'Redirect' );
-               $userTitle = Title::makeTitle( NS_USER, 'Someuser' );
-               $linkCache->addGoodLinkObj(
-                       1, // id
-                       $foobarTitle,
-                       10, // len
-                       0 // redir
-               );
-               $linkCache->addGoodLinkObj(
-                       2, // id
-                       $redirectTitle,
-                       10, // len
-                       1 // redir
-               );
-
-               $linkCache->addGoodLinkObj(
-                       3, // id
-                       $userTitle,
-                       10, // len
-                       0 // redir
-               );
-
-               $this->assertEquals(
-                       '',
-                       Linker::getLinkColour( $foobarTitle, 0 )
-               );
-
-               $this->assertEquals(
-                       'stub',
-                       Linker::getLinkColour( $foobarTitle, 20 )
-               );
-
-               $this->assertEquals(
-                       'mw-redirect',
-                       Linker::getLinkColour( $redirectTitle, 0 )
-               );
-
-               $this->assertEquals(
-                       '',
-                       Linker::getLinkColour( $userTitle, 20 )
-               );
-       }
 }
index a8d1e33..7d7e637 100644 (file)
@@ -154,4 +154,53 @@ class MediaWikiTest extends MediaWikiTestCase {
                        $context->getOutput()->getRedirect()
                );
        }
+
+       /**
+        * Test a post-send job can not set cookies (T191537).
+        */
+       public function testPostSendJobDoesNotSetCookie() {
+               // Prevent updates from running
+               $this->setMwGlobals( 'wgCommandLineMode', false );
+
+               $response = new WebResponse;
+
+               // A job that attempts to set a cookie
+               $jobHasRun = false;
+               DeferredUpdates::addCallableUpdate( function () use ( $response, &$jobHasRun ) {
+                       $jobHasRun = true;
+                       $response->setCookie( 'JobCookie', 'yes' );
+                       $response->header( 'Foo: baz' );
+               } );
+
+               $hookWasRun = false;
+               $this->setTemporaryHook( 'WebResponseSetCookie', function () use ( &$hookWasRun ) {
+                       $hookWasRun = true;
+                       return true;
+               } );
+
+               $logger = new TestLogger();
+               $logger->setCollect( true );
+               $this->setLogger( 'cookie', $logger );
+               $this->setLogger( 'header', $logger );
+
+               $mw = new MediaWiki();
+               $mw->doPostOutputShutdown();
+               // restInPeace() might have been registered to a callback of
+               // register_postsend_function() and thus can not be triggered from
+               // PHPUnit.
+               if ( $jobHasRun === false ) {
+                       $mw->restInPeace();
+               }
+
+               $this->assertTrue( $jobHasRun, 'post-send job has run' );
+               $this->assertFalse( $hookWasRun,
+                       'post-send job must not trigger WebResponseSetCookie hook' );
+               $this->assertEquals(
+                       [
+                               [ 'info', 'ignored post-send cookie {cookie}' ],
+                               [ 'info', 'ignored post-send header {header}' ],
+                       ],
+                       $logger->getBuffer()
+               );
+       }
 }
index ba1249a..73050e0 100644 (file)
@@ -43,6 +43,10 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                );
        }
 
+       protected function addCoreDBData() {
+               // Blank out. This would fail with a modified schema, and we don't need it.
+       }
+
        /**
         * @return int
         */
@@ -262,7 +266,6 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                        [
                                'rev_id',
                                'rev_page',
-                               'rev_text_id',
                                'rev_minor_edit',
                                'rev_deleted',
                                'rev_len',
@@ -273,7 +276,6 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                        [ [
                                strval( $rev->getId() ),
                                strval( $this->testPage->getId() ),
-                               strval( $textId ),
                                '0',
                                '0',
                                '13',
@@ -283,19 +285,52 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                );
        }
 
+       public function provideInsertOn_exceptionOnIncomplete() {
+               $content = new TextContent( '' );
+               $user = User::newFromName( 'Foo' );
+
+               yield 'no parent' => [
+                       [
+                               'content' => $content,
+                               'comment' => 'test',
+                               'user' => $user,
+                       ],
+                       IncompleteRevisionException::class,
+                       "rev_page field must not be 0!"
+               ];
+
+               yield 'no comment' => [
+                       [
+                               'content' => $content,
+                               'page' => 7,
+                               'user' => $user,
+                       ],
+                       IncompleteRevisionException::class,
+                       "comment must not be NULL!"
+               ];
+
+               yield 'no content' => [
+                       [
+                               'comment' => 'test',
+                               'page' => 7,
+                               'user' => $user,
+                       ],
+                       IncompleteRevisionException::class,
+                       "Uninitialized field: content_address" // XXX: message may change
+               ];
+       }
+
        /**
+        * @dataProvider provideInsertOn_exceptionOnIncomplete
         * @covers Revision::insertOn
         */
-       public function testInsertOn_exceptionOnNoPage() {
+       public function testInsertOn_exceptionOnIncomplete( $array, $expException, $expMessage ) {
                // If an ExternalStore is set don't use it.
                $this->setMwGlobals( 'wgDefaultExternalStore', false );
-               $this->setExpectedException(
-                       IncompleteRevisionException::class,
-                       "rev_page field must not be 0!"
-               );
+               $this->setExpectedException( $expException, $expMessage );
 
                $title = Title::newFromText( 'Nonexistant-' . __METHOD__ );
-               $rev = new Revision( [], 0, $title );
+               $rev = new Revision( $array, 0, $title );
 
                $rev->insertOn( wfGetDB( DB_MASTER ) );
        }
@@ -410,6 +445,9 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                        $services->getService( '_SqlBlobStore' ),
                        $services->getMainWANObjectCache(),
                        $services->getCommentStore(),
+                       $services->getContentModelStore(),
+                       $services->getSlotRoleStore(),
+                       $this->getMcrMigrationStage(),
                        $services->getActorMigration()
                );
 
@@ -915,7 +953,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                $rev = new Revision( [
                        'page' => $this->testPage->getId(),
                        'content_model' => $this->testPage->getContentModel(),
-                       'text_id' => 123456789, // not in the test DB
+                       'id' => 123456789, // not in the test DB
                ] );
 
                Wikimedia\suppressWarnings(); // bad text_id will trigger a warning.
@@ -1374,7 +1412,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                $rev = $this->testPage->getRevision();
 
                // Clear any previous cache for the revision during creation
-               $key = $cache->makeGlobalKey( 'revision-row-1.29',
+               $key = $cache->makeGlobalKey( RevisionStore::ROW_CACHE_KEY,
                        $db->getDomainID(),
                        $rev->getPage(),
                        $rev->getId()
diff --git a/tests/phpunit/includes/RevisionMcrDbTest.php b/tests/phpunit/includes/RevisionMcrDbTest.php
new file mode 100644 (file)
index 0000000..3c30efe
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+use MediaWiki\Tests\Storage\McrSchemaOverride;
+
+/**
+ * Tests Revision against the MCR DB schema after schema migration.
+ *
+ * @covers Revision
+ *
+ * @group Revision
+ * @group Storage
+ * @group ContentHandler
+ * @group Database
+ * @group medium
+ */
+class RevisionMcrDbTest extends RevisionDbTestBase {
+
+       use McrSchemaOverride;
+
+       public function setUp() {
+               parent::setUp();
+       }
+
+       protected function getContentHandlerUseDB() {
+               return true;
+       }
+
+}
diff --git a/tests/phpunit/includes/RevisionMcrWriteBothDbTest.php b/tests/phpunit/includes/RevisionMcrWriteBothDbTest.php
new file mode 100644 (file)
index 0000000..436b379
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+use MediaWiki\Tests\Storage\McrWriteBothSchemaOverride;
+
+/**
+ * Tests Revision against the intermediate MCR DB schema for use during schema migration.
+ *
+ * @covers Revision
+ *
+ * @group Revision
+ * @group Storage
+ * @group ContentHandler
+ * @group Database
+ * @group medium
+ */
+class RevisionMcrWriteBothDbTest extends RevisionDbTestBase {
+
+       use McrWriteBothSchemaOverride;
+
+       protected function getContentHandlerUseDB() {
+               return true;
+       }
+
+}
index ab067a4..cf2c64a 100644 (file)
@@ -17,6 +17,11 @@ use Wikimedia\Rdbms\LoadBalancer;
  */
 class RevisionTest extends MediaWikiTestCase {
 
+       public function setUp() {
+               parent::setUp();
+               $this->setMwGlobals( 'wgMultiContentRevisionSchemaMigrationStage', MIGRATION_OLD );
+       }
+
        public function provideConstructFromArray() {
                yield 'with text' => [
                        [
@@ -154,16 +159,15 @@ class RevisionTest extends MediaWikiTestCase {
                                'content' => new WikitextContent( 'GOAT' ),
                                'text_id' => 'someid',
                        ],
-                       new MWException( "Text already stored in external store (id someid), " .
-                               "can't serialize content object" )
+                       new MWException( 'Text already stored in external store (id someid),' )
                ];
                yield 'with bad content object (class)' => [
                        [ 'content' => new stdClass() ],
-                       new MWException( 'content field must contain a Content object.' )
+                       new MWException( 'content field must contain a Content object' )
                ];
                yield 'with bad content object (string)' => [
                        [ 'content' => 'ImAGoat' ],
-                       new MWException( 'content field must contain a Content object.' )
+                       new MWException( 'content field must contain a Content object' )
                ];
                yield 'bad row format' => [
                        'imastring, not a row',
@@ -488,6 +492,9 @@ class RevisionTest extends MediaWikiTestCase {
                        $this->getBlobStore(),
                        $cache,
                        MediaWikiServices::getInstance()->getCommentStore(),
+                       MediaWikiServices::getInstance()->getContentModelStore(),
+                       MediaWikiServices::getInstance()->getSlotRoleStore(),
+                       MIGRATION_OLD,
                        MediaWikiServices::getInstance()->getActorMigration()
                );
                return $blobStore;
@@ -1156,10 +1163,60 @@ class RevisionTest extends MediaWikiTestCase {
                $revisionStore = $this->getRevisionStore();
                $revisionStore->setContentHandlerUseDB( $globals['wgContentHandlerUseDB'] );
                $this->setService( 'RevisionStore', $revisionStore );
-               $this->assertEquals(
-                       $expected,
-                       Revision::getArchiveQueryInfo()
+
+               $queryInfo = Revision::getArchiveQueryInfo();
+
+               $this->assertArrayEqualsIgnoringIntKeyOrder(
+                       $expected['tables'],
+                       $queryInfo['tables']
                );
+               $this->assertArrayEqualsIgnoringIntKeyOrder(
+                       $expected['fields'],
+                       $queryInfo['fields']
+               );
+               $this->assertArrayEqualsIgnoringIntKeyOrder(
+                       $expected['joins'],
+                       $queryInfo['joins']
+               );
+       }
+
+       /**
+        * Assert that the two arrays passed are equal, ignoring the order of the values that integer
+        * keys.
+        *
+        * Note: Failures of this assertion can be slightly confusing as the arrays are actually
+        * split into a string key array and an int key array before assertions occur.
+        *
+        * @param array $expected
+        * @param array $actual
+        */
+       private function assertArrayEqualsIgnoringIntKeyOrder( array $expected, array $actual ) {
+               $this->objectAssociativeSort( $expected );
+               $this->objectAssociativeSort( $actual );
+
+               // Separate the int key values from the string key values so that assertion failures are
+               // easier to understand.
+               $expectedIntKeyValues = [];
+               $actualIntKeyValues = [];
+
+               // Remove all int keys and re add them at the end after sorting by value
+               // This will result in all int keys being in the same order with same ints at the end of
+               // the array
+               foreach ( $expected as $key => $value ) {
+                       if ( is_int( $key ) ) {
+                               unset( $expected[$key] );
+                               $expectedIntKeyValues[] = $value;
+                       }
+               }
+               foreach ( $actual as $key => $value ) {
+                       if ( is_int( $key ) ) {
+                               unset( $actual[$key] );
+                               $actualIntKeyValues[] = $value;
+                       }
+               }
+
+               $this->assertArrayEquals( $expected, $actual, false, true );
+               $this->assertArrayEquals( $expectedIntKeyValues, $actualIntKeyValues, false, true );
        }
 
        public function provideGetQueryInfo() {
@@ -1391,9 +1448,19 @@ class RevisionTest extends MediaWikiTestCase {
                $revisionStore->setContentHandlerUseDB( $globals['wgContentHandlerUseDB'] );
                $this->setService( 'RevisionStore', $revisionStore );
 
-               $this->assertEquals(
-                       $expected,
-                       Revision::getQueryInfo( $options )
+               $queryInfo = Revision::getQueryInfo( $options );
+
+               $this->assertArrayEqualsIgnoringIntKeyOrder(
+                       $expected['tables'],
+                       $queryInfo['tables']
+               );
+               $this->assertArrayEqualsIgnoringIntKeyOrder(
+                       $expected['fields'],
+                       $queryInfo['fields']
+               );
+               $this->assertArrayEqualsIgnoringIntKeyOrder(
+                       $expected['joins'],
+                       $queryInfo['joins']
                );
        }
 
diff --git a/tests/phpunit/includes/Storage/DerivedPageDataUpdaterTest.php b/tests/phpunit/includes/Storage/DerivedPageDataUpdaterTest.php
new file mode 100644 (file)
index 0000000..2924812
--- /dev/null
@@ -0,0 +1,746 @@
+<?php
+
+namespace MediaWiki\Tests\Storage;
+
+use CommentStoreComment;
+use Content;
+use LinksUpdate;
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Storage\DerivedPageDataUpdater;
+use MediaWiki\Storage\MutableRevisionRecord;
+use MediaWiki\Storage\MutableRevisionSlots;
+use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Storage\RevisionSlotsUpdate;
+use MediaWiki\Storage\SlotRecord;
+use MediaWikiTestCase;
+use Title;
+use User;
+use Wikimedia\TestingAccessWrapper;
+use WikiPage;
+use WikitextContent;
+
+/**
+ * @group Database
+ *
+ * @covers MediaWiki\Storage\DerivedPageDataUpdater
+ */
+class DerivedPageDataUpdaterTest extends MediaWikiTestCase {
+
+       /**
+        * @param string $title
+        *
+        * @return Title
+        */
+       private function getTitle( $title ) {
+               return Title::makeTitleSafe( $this->getDefaultWikitextNS(), $title );
+       }
+
+       /**
+        * @param string|Title $title
+        *
+        * @return WikiPage
+        */
+       private function getPage( $title ) {
+               $title = ( $title instanceof Title ) ? $title : $this->getTitle( $title );
+
+               return WikiPage::factory( $title );
+       }
+
+       /**
+        * @param string|Title|WikiPage $page
+        *
+        * @return DerivedPageDataUpdater
+        */
+       private function getDerivedPageDataUpdater( $page, RevisionRecord $rec = null ) {
+               if ( is_string( $page ) || $page instanceof Title ) {
+                       $page = $this->getPage( $page );
+               }
+
+               $page = TestingAccessWrapper::newFromObject( $page );
+               return $page->getDerivedDataUpdater( null, $rec );
+       }
+
+       /**
+        * Creates a revision in the database.
+        *
+        * @param WikiPage $page
+        * @param $summary
+        * @param null|string|Content $content
+        *
+        * @return RevisionRecord|null
+        */
+       private function createRevision( WikiPage $page, $summary, $content = null ) {
+               $user = $this->getTestUser()->getUser();
+               $comment = CommentStoreComment::newUnsavedComment( $summary );
+
+               if ( !$content instanceof Content ) {
+                       $content = new WikitextContent( $content === null ? $summary : $content );
+               }
+
+               $this->getDerivedPageDataUpdater( $page ); // flush cached instance before.
+
+               $updater = $page->newPageUpdater( $user );
+               $updater->setContent( 'main', $content );
+               $rev = $updater->saveRevision( $comment );
+
+               $this->getDerivedPageDataUpdater( $page ); // flush cached instance after.
+               return $rev;
+       }
+
+       // TODO: test setArticleCountMethod() and isCountable();
+       // TODO: test isRedirect() and wasRedirect()
+
+       /**
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getCanonicalParserOptions()
+        */
+       public function testGetCanonicalParserOptions() {
+               global $wgContLang;
+
+               $user = $this->getTestUser()->getUser();
+               $page = $this->getPage( __METHOD__ );
+
+               $parentRev = $this->createRevision( $page, 'first' );
+
+               $mainContent = new WikitextContent( 'Lorem ipsum' );
+
+               $update = new RevisionSlotsUpdate();
+               $update->modifyContent( 'main', $mainContent );
+               $updater = $this->getDerivedPageDataUpdater( $page );
+               $updater->prepareContent( $user, $update, false );
+
+               $options1 = $updater->getCanonicalParserOptions();
+               $this->assertSame( $wgContLang, $options1->getUserLangObj() );
+
+               $speculativeId = call_user_func( $options1->getSpeculativeRevIdCallback(), $page->getTitle() );
+               $this->assertSame( $parentRev->getId() + 1, $speculativeId );
+
+               $rev = $this->makeRevision(
+                       $page->getTitle(),
+                       $update,
+                       $user,
+                       $parentRev->getId() + 7,
+                       $parentRev->getId()
+               );
+               $updater->prepareUpdate( $rev );
+
+               $options2 = $updater->getCanonicalParserOptions();
+               $this->assertNotSame( $options1, $options2 );
+
+               $currentRev = call_user_func( $options2->getCurrentRevisionCallback(), $page->getTitle() );
+               $this->assertSame( $rev->getId(), $currentRev->getId() );
+       }
+
+       /**
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::grabCurrentRevision()
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::pageExisted()
+        */
+       public function testGrabCurrentRevision() {
+               $page = $this->getPage( __METHOD__ );
+
+               $updater0 = $this->getDerivedPageDataUpdater( $page );
+               $this->assertNull( $updater0->grabCurrentRevision() );
+               $this->assertFalse( $updater0->pageExisted() );
+
+               $rev1 = $this->createRevision( $page, 'first' );
+               $updater1 = $this->getDerivedPageDataUpdater( $page );
+               $this->assertSame( $rev1->getId(), $updater1->grabCurrentRevision()->getId() );
+               $this->assertFalse( $updater0->pageExisted() );
+               $this->assertTrue( $updater1->pageExisted() );
+
+               $rev2 = $this->createRevision( $page, 'second' );
+               $updater2 = $this->getDerivedPageDataUpdater( $page );
+               $this->assertSame( $rev1->getId(), $updater1->grabCurrentRevision()->getId() );
+               $this->assertSame( $rev2->getId(), $updater2->grabCurrentRevision()->getId() );
+       }
+
+       /**
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareContent()
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isContentPrepared()
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::pageExisted()
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isCreation()
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isChange()
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlots()
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getRawSlot()
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getRawContent()
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getModifiedSlotRoles()
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getTouchedSlotRoles()
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlotParserOutput()
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getCanonicalParserOutput()
+        */
+       public function testPrepareContent() {
+               $user = $this->getTestUser()->getUser();
+               $updater = $this->getDerivedPageDataUpdater( __METHOD__ );
+
+               $this->assertFalse( $updater->isContentPrepared() );
+
+               // TODO: test stash
+               // TODO: MCR: Test multiple slots. Test slot removal.
+               $mainContent = new WikitextContent( 'first [[main]] ~~~' );
+               $auxContent = new WikitextContent( 'inherited ~~~ content' );
+               $auxSlot = SlotRecord::newSaved(
+                       10, 7, 'tt:7',
+                       SlotRecord::newUnsaved( 'aux', $auxContent )
+               );
+
+               $update = new RevisionSlotsUpdate();
+               $update->modifyContent( 'main', $mainContent );
+               $update->modifySlot( SlotRecord::newInherited( $auxSlot ) );
+               // TODO: MCR: test removing slots!
+
+               $updater->prepareContent( $user, $update, false );
+
+               // second be ok to call again with the same params
+               $updater->prepareContent( $user, $update, false );
+
+               $this->assertNull( $updater->grabCurrentRevision() );
+               $this->assertTrue( $updater->isContentPrepared() );
+               $this->assertFalse( $updater->isUpdatePrepared() );
+               $this->assertFalse( $updater->pageExisted() );
+               $this->assertTrue( $updater->isCreation() );
+               $this->assertTrue( $updater->isChange() );
+               $this->assertTrue( $updater->isContentPublic() );
+
+               $this->assertEquals( [ 'main', 'aux' ], $updater->getSlots()->getSlotRoles() );
+               $this->assertEquals( [ 'main' ], array_keys( $updater->getSlots()->getOriginalSlots() ) );
+               $this->assertEquals( [ 'aux' ], array_keys( $updater->getSlots()->getInheritedSlots() ) );
+               $this->assertEquals( [ 'main', 'aux' ], $updater->getModifiedSlotRoles() );
+               $this->assertEquals( [ 'main', 'aux' ], $updater->getTouchedSlotRoles() );
+
+               $mainSlot = $updater->getRawSlot( 'main' );
+               $this->assertInstanceOf( SlotRecord::class, $mainSlot );
+               $this->assertNotContains( '~~~', $mainSlot->getContent()->serialize(), 'PST should apply.' );
+               $this->assertContains( $user->getName(), $mainSlot->getContent()->serialize() );
+
+               $auxSlot = $updater->getRawSlot( 'aux' );
+               $this->assertInstanceOf( SlotRecord::class, $auxSlot );
+               $this->assertContains( '~~~', $auxSlot->getContent()->serialize(), 'No PST should apply.' );
+
+               $mainOutput = $updater->getCanonicalParserOutput();
+               $this->assertContains( 'first', $mainOutput->getText() );
+               $this->assertContains( '<a ', $mainOutput->getText() );
+               $this->assertNotEmpty( $mainOutput->getLinks() );
+
+               $canonicalOutput = $updater->getCanonicalParserOutput();
+               $this->assertContains( 'first', $canonicalOutput->getText() );
+               $this->assertContains( '<a ', $canonicalOutput->getText() );
+               $this->assertNotEmpty( $canonicalOutput->getLinks() );
+       }
+
+       /**
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareContent()
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::pageExisted()
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isCreation()
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isChange()
+        */
+       public function testPrepareContentInherit() {
+               $user = $this->getTestUser()->getUser();
+               $page = $this->getPage( __METHOD__ );
+
+               $mainContent1 = new WikitextContent( 'first [[main]] ~~~' );
+               $mainContent2 = new WikitextContent( 'second' );
+
+               $this->createRevision( $page, 'first', $mainContent1 );
+
+               $update = new RevisionSlotsUpdate();
+               $update->modifyContent( 'main', $mainContent1 );
+               $updater1 = $this->getDerivedPageDataUpdater( $page );
+               $updater1->prepareContent( $user, $update, false );
+
+               $this->assertNotNull( $updater1->grabCurrentRevision() );
+               $this->assertTrue( $updater1->isContentPrepared() );
+               $this->assertTrue( $updater1->pageExisted() );
+               $this->assertFalse( $updater1->isCreation() );
+               $this->assertFalse( $updater1->isChange() );
+
+               // TODO: MCR: test inheritance from parent
+               $update = new RevisionSlotsUpdate();
+               $update->modifyContent( 'main', $mainContent2 );
+               $updater2 = $this->getDerivedPageDataUpdater( $page );
+               $updater2->prepareContent( $user, $update, false );
+
+               $this->assertFalse( $updater2->isCreation() );
+               $this->assertTrue( $updater2->isChange() );
+       }
+
+       // TODO: test failure of prepareContent() when called again...
+       // - with different user
+       // - with different update
+       // - after calling prepareUpdate()
+
+       /**
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareUpdate()
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isUpdatePrepared()
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isCreation()
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlots()
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getRawSlot()
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getRawContent()
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getModifiedSlotRoles()
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getTouchedSlotRoles()
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlotParserOutput()
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getCanonicalParserOutput()
+        */
+       public function testPrepareUpdate() {
+               $page = $this->getPage( __METHOD__ );
+
+               $mainContent1 = new WikitextContent( 'first [[main]] ~~~' );
+               $rev1 = $this->createRevision( $page, 'first', $mainContent1 );
+               $updater1 = $this->getDerivedPageDataUpdater( $page, $rev1 );
+
+               $options = []; // TODO: test *all* the options...
+               $updater1->prepareUpdate( $rev1, $options );
+
+               $this->assertTrue( $updater1->isUpdatePrepared() );
+               $this->assertTrue( $updater1->isContentPrepared() );
+               $this->assertTrue( $updater1->isCreation() );
+               $this->assertTrue( $updater1->isChange() );
+               $this->assertTrue( $updater1->isContentPublic() );
+
+               $this->assertEquals( [ 'main' ], $updater1->getSlots()->getSlotRoles() );
+               $this->assertEquals( [ 'main' ], array_keys( $updater1->getSlots()->getOriginalSlots() ) );
+               $this->assertEquals( [], array_keys( $updater1->getSlots()->getInheritedSlots() ) );
+               $this->assertEquals( [ 'main' ], $updater1->getModifiedSlotRoles() );
+               $this->assertEquals( [ 'main' ], $updater1->getTouchedSlotRoles() );
+
+               // TODO: MCR: test multiple slots, test slot removal!
+
+               $this->assertInstanceOf( SlotRecord::class, $updater1->getRawSlot( 'main' ) );
+               $this->assertNotContains( '~~~~', $updater1->getRawContent( 'main' )->serialize() );
+
+               $mainOutput = $updater1->getCanonicalParserOutput();
+               $this->assertContains( 'first', $mainOutput->getText() );
+               $this->assertContains( '<a ', $mainOutput->getText() );
+               $this->assertNotEmpty( $mainOutput->getLinks() );
+
+               $canonicalOutput = $updater1->getCanonicalParserOutput();
+               $this->assertContains( 'first', $canonicalOutput->getText() );
+               $this->assertContains( '<a ', $canonicalOutput->getText() );
+               $this->assertNotEmpty( $canonicalOutput->getLinks() );
+
+               $mainContent2 = new WikitextContent( 'second' );
+               $rev2 = $this->createRevision( $page, 'second', $mainContent2 );
+               $updater2 = $this->getDerivedPageDataUpdater( $page, $rev2 );
+
+               $options = []; // TODO: test *all* the options...
+               $updater2->prepareUpdate( $rev2, $options );
+
+               $this->assertFalse( $updater2->isCreation() );
+               $this->assertTrue( $updater2->isChange() );
+
+               $canonicalOutput = $updater2->getCanonicalParserOutput();
+               $this->assertContains( 'second', $canonicalOutput->getText() );
+       }
+
+       /**
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareUpdate()
+        */
+       public function testPrepareUpdateReusesParserOutput() {
+               $user = $this->getTestUser()->getUser();
+               $page = $this->getPage( __METHOD__ );
+
+               $mainContent1 = new WikitextContent( 'first [[main]] ~~~' );
+
+               $update = new RevisionSlotsUpdate();
+               $update->modifyContent( 'main', $mainContent1 );
+               $updater = $this->getDerivedPageDataUpdater( $page );
+               $updater->prepareContent( $user, $update, false );
+
+               $mainOutput = $updater->getSlotParserOutput( 'main' );
+               $canonicalOutput = $updater->getCanonicalParserOutput();
+
+               $rev = $this->createRevision( $page, 'first', $mainContent1 );
+
+               $options = []; // TODO: test *all* the options...
+               $updater->prepareUpdate( $rev, $options );
+
+               $this->assertTrue( $updater->isUpdatePrepared() );
+               $this->assertTrue( $updater->isContentPrepared() );
+
+               $this->assertSame( $mainOutput, $updater->getSlotParserOutput( 'main' ) );
+               $this->assertSame( $canonicalOutput, $updater->getCanonicalParserOutput() );
+       }
+
+       /**
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareUpdate()
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlotParserOutput()
+        */
+       public function testPrepareUpdateOutputReset() {
+               $user = $this->getTestUser()->getUser();
+               $page = $this->getPage( __METHOD__ );
+
+               $mainContent1 = new WikitextContent( 'first --{{REVISIONID}}--' );
+
+               $update = new RevisionSlotsUpdate();
+               $update->modifyContent( 'main', $mainContent1 );
+               $updater = $this->getDerivedPageDataUpdater( $page );
+               $updater->prepareContent( $user, $update, false );
+
+               $mainOutput = $updater->getSlotParserOutput( 'main' );
+               $canonicalOutput = $updater->getCanonicalParserOutput();
+
+               // prevent optimization on matching speculative ID
+               $mainOutput->setSpeculativeRevIdUsed( 0 );
+               $canonicalOutput->setSpeculativeRevIdUsed( 0 );
+
+               $rev = $this->createRevision( $page, 'first', $mainContent1 );
+
+               $options = []; // TODO: test *all* the options...
+               $updater->prepareUpdate( $rev, $options );
+
+               $this->assertTrue( $updater->isUpdatePrepared() );
+               $this->assertTrue( $updater->isContentPrepared() );
+
+               // ParserOutput objects should have been flushed.
+               $this->assertNotSame( $mainOutput, $updater->getSlotParserOutput( 'main' ) );
+               $this->assertNotSame( $canonicalOutput, $updater->getCanonicalParserOutput() );
+
+               $html = $updater->getCanonicalParserOutput()->getText();
+               $this->assertContains( '--' . $rev->getId() . '--', $html );
+
+               // TODO: MCR: ensure that when the main slot uses {{REVISIONID}} but another slot is
+               // updated, the main slot is still re-rendered!
+       }
+
+       // TODO: test failure of prepareUpdate() when called again with a different revision
+       // TODO: test failure of prepareUpdate() on inconsistency with prepareContent.
+
+       /**
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getPreparedEdit()
+        */
+       public function testGetPreparedEditAfterPrepareContent() {
+               $user = $this->getTestUser()->getUser();
+
+               $mainContent = new WikitextContent( 'first [[main]] ~~~' );
+               $update = new RevisionSlotsUpdate();
+               $update->modifyContent( 'main', $mainContent );
+
+               $updater = $this->getDerivedPageDataUpdater( __METHOD__ );
+               $updater->prepareContent( $user, $update, false );
+
+               $canonicalOutput = $updater->getCanonicalParserOutput();
+
+               $preparedEdit = $updater->getPreparedEdit();
+               $this->assertSame( $canonicalOutput->getCacheTime(), $preparedEdit->timestamp );
+               $this->assertSame( $canonicalOutput, $preparedEdit->output );
+               $this->assertSame( $mainContent, $preparedEdit->newContent );
+               $this->assertSame( $updater->getRawContent( 'main' ), $preparedEdit->pstContent );
+               $this->assertSame( $updater->getCanonicalParserOptions(), $preparedEdit->popts );
+               $this->assertSame( null, $preparedEdit->revid );
+       }
+
+       /**
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getPreparedEdit()
+        */
+       public function testGetPreparedEditAfterPrepareUpdate() {
+               $page = $this->getPage( __METHOD__ );
+
+               $mainContent = new WikitextContent( 'first [[main]] ~~~' );
+               $update = new MutableRevisionSlots();
+               $update->setContent( 'main', $mainContent );
+
+               $rev = $this->createRevision( $page, __METHOD__ );
+
+               $updater = $this->getDerivedPageDataUpdater( $page );
+               $updater->prepareUpdate( $rev );
+
+               $canonicalOutput = $updater->getCanonicalParserOutput();
+
+               $preparedEdit = $updater->getPreparedEdit();
+               $this->assertSame( $canonicalOutput->getCacheTime(), $preparedEdit->timestamp );
+               $this->assertSame( $canonicalOutput, $preparedEdit->output );
+               $this->assertSame( $updater->getRawContent( 'main' ), $preparedEdit->pstContent );
+               $this->assertSame( $updater->getCanonicalParserOptions(), $preparedEdit->popts );
+               $this->assertSame( $rev->getId(), $preparedEdit->revid );
+       }
+
+       public function testGetSecondaryDataUpdatesAfterPrepareContent() {
+               $user = $this->getTestUser()->getUser();
+               $page = $this->getPage( __METHOD__ );
+               $this->createRevision( $page, __METHOD__ );
+
+               $mainContent1 = new WikitextContent( 'first' );
+
+               $update = new RevisionSlotsUpdate();
+               $update->modifyContent( 'main', $mainContent1 );
+               $updater = $this->getDerivedPageDataUpdater( $page );
+               $updater->prepareContent( $user, $update, false );
+
+               $dataUpdates = $updater->getSecondaryDataUpdates();
+
+               // TODO: MCR: assert updates from all slots!
+               $this->assertNotEmpty( $dataUpdates );
+
+               $linksUpdates = array_filter( $dataUpdates, function ( $du ) {
+                       return $du instanceof LinksUpdate;
+               } );
+               $this->assertCount( 1, $linksUpdates );
+       }
+
+       /**
+        * Creates a dummy revision object without touching the database.
+        *
+        * @param Title $title
+        * @param RevisionSlotsUpdate $update
+        * @param User $user
+        * @param string $comment
+        * @param int $id
+        * @param int $parentId
+        *
+        * @return MutableRevisionRecord
+        */
+       private function makeRevision(
+               Title $title,
+               RevisionSlotsUpdate $update,
+               User $user,
+               $comment,
+               $id,
+               $parentId = 0
+       ) {
+               $rev = new MutableRevisionRecord( $title );
+
+               $rev->applyUpdate( $update );
+               $rev->setUser( $user );
+               $rev->setComment( CommentStoreComment::newUnsavedComment( $comment ) );
+               $rev->setId( $id );
+               $rev->setPageId( $title->getArticleID() );
+               $rev->setParentId( $parentId );
+
+               return $rev;
+       }
+
+       public function provideIsReusableFor() {
+               $title = Title::makeTitleSafe( NS_MAIN, __METHOD__ );
+
+               $user1 = User::newFromName( 'Alice' );
+               $user2 = User::newFromName( 'Bob' );
+
+               $content1 = new WikitextContent( 'one' );
+               $content2 = new WikitextContent( 'two' );
+
+               $update1 = new RevisionSlotsUpdate();
+               $update1->modifyContent( 'main', $content1 );
+
+               $update1b = new RevisionSlotsUpdate();
+               $update1b->modifyContent( 'xyz', $content1 );
+
+               $update2 = new RevisionSlotsUpdate();
+               $update2->modifyContent( 'main', $content2 );
+
+               $rev1 = $this->makeRevision( $title, $update1, $user1, 'rev1', 11 );
+               $rev1b = $this->makeRevision( $title, $update1b, $user1, 'rev1', 11 );
+
+               $rev2 = $this->makeRevision( $title, $update2, $user1, 'rev2', 12 );
+               $rev2x = $this->makeRevision( $title, $update2, $user2, 'rev2', 12 );
+               $rev2y = $this->makeRevision( $title, $update2, $user1, 'rev2', 122 );
+
+               yield 'any' => [
+                       '$prepUser' => null,
+                       '$prepRevision' => null,
+                       '$prepUpdate' => null,
+                       '$forUser' => null,
+                       '$forRevision' => null,
+                       '$forUpdate' => null,
+                       '$forParent' => null,
+                       '$isReusable' => true,
+               ];
+               yield 'for any' => [
+                       '$prepUser' => $user1,
+                       '$prepRevision' => $rev1,
+                       '$prepUpdate' => $update1,
+                       '$forUser' => null,
+                       '$forRevision' => null,
+                       '$forUpdate' => null,
+                       '$forParent' => null,
+                       '$isReusable' => true,
+               ];
+               yield 'unprepared' => [
+                       '$prepUser' => null,
+                       '$prepRevision' => null,
+                       '$prepUpdate' => null,
+                       '$forUser' => $user1,
+                       '$forRevision' => $rev1,
+                       '$forUpdate' => $update1,
+                       '$forParent' => 0,
+                       '$isReusable' => true,
+               ];
+               yield 'match prepareContent' => [
+                       '$prepUser' => $user1,
+                       '$prepRevision' => null,
+                       '$prepUpdate' => $update1,
+                       '$forUser' => $user1,
+                       '$forRevision' => null,
+                       '$forUpdate' => $update1,
+                       '$forParent' => 0,
+                       '$isReusable' => true,
+               ];
+               yield 'match prepareUpdate' => [
+                       '$prepUser' => null,
+                       '$prepRevision' => $rev1,
+                       '$prepUpdate' => null,
+                       '$forUser' => $user1,
+                       '$forRevision' => $rev1,
+                       '$forUpdate' => null,
+                       '$forParent' => 0,
+                       '$isReusable' => true,
+               ];
+               yield 'match all' => [
+                       '$prepUser' => $user1,
+                       '$prepRevision' => $rev1,
+                       '$prepUpdate' => $update1,
+                       '$forUser' => $user1,
+                       '$forRevision' => $rev1,
+                       '$forUpdate' => $update1,
+                       '$forParent' => 0,
+                       '$isReusable' => true,
+               ];
+               yield 'mismatch prepareContent update' => [
+                       '$prepUser' => $user1,
+                       '$prepRevision' => null,
+                       '$prepUpdate' => $update1,
+                       '$forUser' => $user1,
+                       '$forRevision' => null,
+                       '$forUpdate' => $update1b,
+                       '$forParent' => 0,
+                       '$isReusable' => false,
+               ];
+               yield 'mismatch prepareContent user' => [
+                       '$prepUser' => $user1,
+                       '$prepRevision' => null,
+                       '$prepUpdate' => $update1,
+                       '$forUser' => $user2,
+                       '$forRevision' => null,
+                       '$forUpdate' => $update1,
+                       '$forParent' => 0,
+                       '$isReusable' => false,
+               ];
+               yield 'mismatch prepareContent parent' => [
+                       '$prepUser' => $user1,
+                       '$prepRevision' => null,
+                       '$prepUpdate' => $update1,
+                       '$forUser' => $user1,
+                       '$forRevision' => null,
+                       '$forUpdate' => $update1,
+                       '$forParent' => 7,
+                       '$isReusable' => false,
+               ];
+               yield 'mismatch prepareUpdate revision update' => [
+                       '$prepUser' => null,
+                       '$prepRevision' => $rev1,
+                       '$prepUpdate' => null,
+                       '$forUser' => null,
+                       '$forRevision' => $rev1b,
+                       '$forUpdate' => null,
+                       '$forParent' => 0,
+                       '$isReusable' => false,
+               ];
+               yield 'mismatch prepareUpdate revision user' => [
+                       '$prepUser' => null,
+                       '$prepRevision' => $rev2,
+                       '$prepUpdate' => null,
+                       '$forUser' => null,
+                       '$forRevision' => $rev2x,
+                       '$forUpdate' => null,
+                       '$forParent' => 0,
+                       '$isReusable' => false,
+               ];
+               yield 'mismatch prepareUpdate revision id' => [
+                       '$prepUser' => null,
+                       '$prepRevision' => $rev2,
+                       '$prepUpdate' => null,
+                       '$forUser' => null,
+                       '$forRevision' => $rev2y,
+                       '$forUpdate' => null,
+                       '$forParent' => 0,
+                       '$isReusable' => false,
+               ];
+       }
+
+       /**
+        * @dataProvider provideIsReusableFor
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isReusableFor()
+        *
+        * @param User|null $prepUser
+        * @param RevisionRecord|null $prepRevision
+        * @param RevisionSlotsUpdate|null $prepUpdate
+        * @param User|null $forUser
+        * @param RevisionRecord|null $forRevision
+        * @param RevisionSlotsUpdate|null $forUpdate
+        * @param int|null $forParent
+        * @param bool $isReusable
+        */
+       public function testIsReusableFor(
+               User $prepUser = null,
+               RevisionRecord $prepRevision = null,
+               RevisionSlotsUpdate $prepUpdate = null,
+               User $forUser = null,
+               RevisionRecord $forRevision = null,
+               RevisionSlotsUpdate $forUpdate = null,
+               $forParent = null,
+               $isReusable = null
+       ) {
+               $updater = $this->getDerivedPageDataUpdater( __METHOD__ );
+
+               if ( $prepUpdate ) {
+                       $updater->prepareContent( $prepUser, $prepUpdate, false );
+               }
+
+               if ( $prepRevision ) {
+                       $updater->prepareUpdate( $prepRevision );
+               }
+
+               $this->assertSame(
+                       $isReusable,
+                       $updater->isReusableFor( $forUser, $forRevision, $forUpdate, $forParent )
+               );
+       }
+
+       /**
+        * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doUpdates()
+        */
+       public function testDoUpdates() {
+               $page = $this->getPage( __METHOD__ );
+
+               $mainContent1 = new WikitextContent( 'first [[main]]' );
+               $rev = $this->createRevision( $page, 'first', $mainContent1 );
+               $pageId = $page->getId();
+               $oldStats = $this->db->selectRow( 'site_stats', '*', '1=1' );
+
+               $updater = $this->getDerivedPageDataUpdater( $page, $rev );
+               $updater->setArticleCountMethod( 'link' );
+
+               $options = []; // TODO: test *all* the options...
+               $updater->prepareUpdate( $rev, $options );
+
+               $updater->doUpdates();
+
+               // links table update
+               $linkCount = $this->db->selectRowCount( 'pagelinks', '*', [ 'pl_from' => $pageId ] );
+               $this->assertSame( 1, $linkCount );
+
+               $pageLinksRow = $this->db->selectRow( 'pagelinks', '*', [ 'pl_from' => $pageId ] );
+               $this->assertInternalType( 'object', $pageLinksRow );
+               $this->assertSame( 'Main', $pageLinksRow->pl_title );
+
+               // parser cache update
+               $pcache = MediaWikiServices::getInstance()->getParserCache();
+               $cached = $pcache->get( $page, $updater->getCanonicalParserOptions() );
+               $this->assertInternalType( 'object', $cached );
+               $this->assertSame( $updater->getCanonicalParserOutput(), $cached );
+
+               // site stats
+               $stats = $this->db->selectRow( 'site_stats', '*', '1=1' );
+               $this->assertSame( $oldStats->ss_total_pages + 1, (int)$stats->ss_total_pages );
+               $this->assertSame( $oldStats->ss_total_edits + 1, (int)$stats->ss_total_edits );
+               $this->assertSame( $oldStats->ss_good_articles + 1, (int)$stats->ss_good_articles );
+
+               // TODO: MCR: test data updates for additional slots!
+               // TODO: test update for edit without page creation
+               // TODO: test message cache purge
+               // TODO: test module cache purge
+               // TODO: test CDN purge
+               // TODO: test newtalk update
+               // TODO: test search update
+               // TODO: test site stats good_articles while turning the page into (or back from) a redir.
+               // TODO: test category membership update (with setRcWatchCategoryMembership())
+       }
+
+}
diff --git a/tests/phpunit/includes/Storage/McrRevisionStoreDbTest.php b/tests/phpunit/includes/Storage/McrRevisionStoreDbTest.php
new file mode 100644 (file)
index 0000000..5bf49d3
--- /dev/null
@@ -0,0 +1,248 @@
+<?php
+namespace MediaWiki\Tests\Storage;
+
+use CommentStoreComment;
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Storage\SlotRecord;
+use TextContent;
+use Title;
+use WikitextContent;
+
+/**
+ * Tests RevisionStore against the post-migration MCR DB schema.
+ *
+ * @covers \MediaWiki\Storage\RevisionStore
+ *
+ * @group RevisionStore
+ * @group Storage
+ * @group Database
+ * @group medium
+ */
+class McrRevisionStoreDbTest extends RevisionStoreDbTestBase {
+
+       use McrSchemaOverride;
+
+       protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) {
+               $numberOfSlots = count( $rev->getSlotRoles() );
+
+               $this->assertSelect(
+                       'slots',
+                       [ 'count(*)' ],
+                       [ 'slot_revision_id' => $rev->getId() ],
+                       [ [ (string)$numberOfSlots ] ]
+               );
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $revQuery = $store->getSlotsQueryInfo( [ 'content' ] );
+
+               $this->assertSelect(
+                       $revQuery['tables'],
+                       [ 'count(*)' ],
+                       [
+                               'slot_revision_id' => $rev->getId(),
+                       ],
+                       [ [ (string)$numberOfSlots ] ],
+                       [],
+                       $revQuery['joins']
+               );
+
+               $this->assertSelect(
+                       'content',
+                       [ 'count(*)' ],
+                       [ 'content_address' => $rev->getSlot( 'main' )->getAddress() ],
+                       [ [ 1 ] ]
+               );
+
+               parent::assertRevisionExistsInDatabase( $rev );
+       }
+
+       /**
+        * @param SlotRecord $a
+        * @param SlotRecord $b
+        */
+       protected function assertSameSlotContent( SlotRecord $a, SlotRecord $b ) {
+               parent::assertSameSlotContent( $a, $b );
+
+               // Assert that the same content ID has been used
+               $this->assertSame( $a->getContentId(), $b->getContentId() );
+       }
+
+       public function provideInsertRevisionOn_successes() {
+               foreach ( parent::provideInsertRevisionOn_successes() as $case ) {
+                       yield $case;
+               }
+
+               yield 'Multi-slot revision insertion' => [
+                       [
+                               'content' => [
+                                       'main' => new WikitextContent( 'Chicken' ),
+                                       'aux' => new TextContent( 'Egg' ),
+                               ],
+                               'page' => true,
+                               'comment' => $this->getRandomCommentStoreComment(),
+                               'timestamp' => '20171117010101',
+                               'user' => true,
+                       ],
+               ];
+       }
+
+       public function provideNewNullRevision() {
+               foreach ( parent::provideNewNullRevision() as $case ) {
+                       yield $case;
+               }
+
+               yield [
+                       Title::newFromText( 'UTPage_notAutoCreated' ),
+                       [
+                               'content' => [
+                                       'main' => new WikitextContent( 'Chicken' ),
+                                       'aux' => new WikitextContent( 'Omelet' ),
+                               ],
+                       ],
+                       CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment multi' ),
+               ];
+       }
+
+       public function provideNewMutableRevisionFromArray() {
+               foreach ( parent::provideNewMutableRevisionFromArray() as $case ) {
+                       yield $case;
+               }
+
+               yield 'Basic array, multiple roles' => [
+                       [
+                               'id' => 2,
+                               'page' => 1,
+                               'timestamp' => '20171017114835',
+                               'user_text' => '111.0.1.2',
+                               'user' => 0,
+                               'minor_edit' => false,
+                               'deleted' => 0,
+                               'len' => 29,
+                               'parent_id' => 1,
+                               'sha1' => '89qs83keq9c9ccw9olvvm4oc9oq50ii',
+                               'comment' => 'Goat Comment!',
+                               'content' => [
+                                       'main' => new WikitextContent( 'Söme Cöntent' ),
+                                       'aux' => new TextContent( 'Öther Cöntent' ),
+                               ]
+                       ]
+               ];
+       }
+
+       public function testGetQueryInfo_NoSlotDataJoin() {
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $queryInfo = $store->getQueryInfo();
+
+               // with the new schema enabled, query info should not join the main slot info
+               $this->assertFalse( array_key_exists( 'a_slot_data', $queryInfo['tables'] ) );
+               $this->assertFalse( array_key_exists( 'a_slot_data', $queryInfo['joins'] ) );
+       }
+
+       public function provideGetArchiveQueryInfo() {
+               yield [
+                       [
+                               'tables' => [
+                                       'archive',
+                               ],
+                               'fields' => array_merge(
+                                       $this->getDefaultArchiveFields( false ),
+                                       [
+                                               'ar_comment_text' => 'ar_comment',
+                                               'ar_comment_data' => 'NULL',
+                                               'ar_comment_cid' => 'NULL',
+                                               'ar_user_text' => 'ar_user_text',
+                                               'ar_user' => 'ar_user',
+                                               'ar_actor' => 'NULL',
+                                       ]
+                               ),
+                               'joins' => [
+                               ],
+                       ]
+               ];
+       }
+
+       public function provideGetQueryInfo() {
+               // TODO: more option variations
+               yield [
+                       [ 'page', 'user' ],
+                       [
+                               'tables' => [
+                                       'revision',
+                                       'page',
+                                       'user',
+                               ],
+                               'fields' => array_merge(
+                                       $this->getDefaultQueryFields( false ),
+                                       $this->getCommentQueryFields(),
+                                       $this->getActorQueryFields(),
+                                       [
+                                               'page_namespace',
+                                               'page_title',
+                                               'page_id',
+                                               'page_latest',
+                                               'page_is_redirect',
+                                               'page_len',
+                                               'user_name',
+                                       ]
+                               ),
+                               'joins' => [
+                                       'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
+                                       'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
+                               ],
+                       ]
+               ];
+       }
+
+       public function provideGetSlotsQueryInfo() {
+               yield [
+                       [],
+                       [
+                               'tables' => [
+                                       'slots',
+                                       'slot_roles',
+                               ],
+                               'fields' => array_merge(
+                                       [
+                                               'slot_revision_id',
+                                               'slot_content_id',
+                                               'slot_origin',
+                                               'role_name',
+                                       ]
+                               ),
+                               'joins' => [
+                                       'slot_roles' => [ 'INNER JOIN', [ 'slot_role_id = role_id' ] ],
+                               ],
+                       ]
+               ];
+               yield [
+                       [ 'content' ],
+                       [
+                               'tables' => [
+                                       'slots',
+                                       'slot_roles',
+                                       'content',
+                                       'content_models',
+                               ],
+                               'fields' => array_merge(
+                                       [
+                                               'slot_revision_id',
+                                               'slot_content_id',
+                                               'slot_origin',
+                                               'role_name',
+                                               'content_size',
+                                               'content_sha1',
+                                               'content_address',
+                                               'model_name',
+                                       ]
+                               ),
+                               'joins' => [
+                                       'slot_roles' => [ 'INNER JOIN', [ 'slot_role_id = role_id' ] ],
+                                       'content' => [ 'INNER JOIN', [ 'slot_content_id = content_id' ] ],
+                                       'content_models' => [ 'INNER JOIN', [ 'content_model = model_id' ] ],
+                               ],
+                       ]
+               ];
+       }
+
+}
diff --git a/tests/phpunit/includes/Storage/McrSchemaOverride.php b/tests/phpunit/includes/Storage/McrSchemaOverride.php
new file mode 100644 (file)
index 0000000..d2f58bf
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+namespace MediaWiki\Tests\Storage;
+
+use Wikimedia\Rdbms\IMaintainableDatabase;
+use MediaWiki\DB\PatchFileLocation;
+
+/**
+ * Trait providing schema overrides that allow tests to run against the post-migration
+ * MCR database schema.
+ */
+trait McrSchemaOverride {
+
+       use PatchFileLocation;
+       use McrSchemaDetection;
+
+       /**
+        * @return int
+        */
+       protected function getMcrMigrationStage() {
+               return MIGRATION_NEW;
+       }
+
+       /**
+        * @return string[]
+        */
+       protected function getMcrTablesToReset() {
+               return [
+                       'content',
+                       'content_models',
+                       'slots',
+                       'slot_roles',
+               ];
+       }
+
+       protected function getSchemaOverrides( IMaintainableDatabase $db ) {
+               $overrides = [
+                       'scripts' => [],
+                       'drop' => [],
+                       'create' => [],
+                       'alter' => [],
+               ];
+
+               if ( !$this->hasMcrTables( $db ) ) {
+                       $overrides['create'] = [ 'slots', 'content', 'slot_roles', 'content_models', ];
+                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slot_roles' );
+                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content_models' );
+                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content' );
+                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slots.sql' );
+               }
+
+               if ( !$this->hasPreMcrFields( $db ) ) {
+                       $overrides['alter'][] = 'revision';
+                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'drop-pre-mcr-fields', __DIR__ );
+               }
+
+               return $overrides;
+       }
+
+}
diff --git a/tests/phpunit/includes/Storage/McrWriteBothRevisionStoreDbTest.php b/tests/phpunit/includes/Storage/McrWriteBothRevisionStoreDbTest.php
new file mode 100644 (file)
index 0000000..c984142
--- /dev/null
@@ -0,0 +1,216 @@
+<?php
+namespace MediaWiki\Tests\Storage;
+
+use InvalidArgumentException;
+use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Storage\SlotRecord;
+use Revision;
+use WikitextContent;
+
+/**
+ * Tests RevisionStore against the intermediate MCR DB schema for use during schema migration.
+ *
+ * @covers \MediaWiki\Storage\RevisionStore
+ *
+ * @group RevisionStore
+ * @group Storage
+ * @group Database
+ * @group medium
+ */
+class McrWriteBothRevisionStoreDbTest extends RevisionStoreDbTestBase {
+
+       use McrWriteBothSchemaOverride;
+
+       protected function revisionToRow( Revision $rev, $options = [ 'page', 'user', 'comment' ] ) {
+               $row = parent::revisionToRow( $rev, $options );
+
+               $row->rev_text_id = (string)$rev->getTextId();
+               $row->rev_content_format = (string)$rev->getContentFormat();
+               $row->rev_content_model = (string)$rev->getContentModel();
+
+               return $row;
+       }
+
+       protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) {
+               $this->assertSelect(
+                       'slots',
+                       [ 'count(*)' ],
+                       [ 'slot_revision_id' => $rev->getId() ],
+                       [ [ '1' ] ]
+               );
+
+               $this->assertSelect(
+                       'content',
+                       [ 'count(*)' ],
+                       [ 'content_address' => $rev->getSlot( 'main' )->getAddress() ],
+                       [ [ '1' ] ]
+               );
+
+               parent::assertRevisionExistsInDatabase( $rev );
+       }
+
+       /**
+        * @param SlotRecord $a
+        * @param SlotRecord $b
+        */
+       protected function assertSameSlotContent( SlotRecord $a, SlotRecord $b ) {
+               parent::assertSameSlotContent( $a, $b );
+
+               // Assert that the same content ID has been used
+               if ( $a->hasContentId() && $b->hasContentId() ) {
+                       $this->assertSame( $a->getContentId(), $b->getContentId() );
+               }
+       }
+
+       public function provideGetArchiveQueryInfo() {
+               yield [
+                       [
+                               'tables' => [ 'archive' ],
+                               'fields' => array_merge(
+                                       $this->getDefaultArchiveFields(),
+                                       [
+                                               'ar_comment_text' => 'ar_comment',
+                                               'ar_comment_data' => 'NULL',
+                                               'ar_comment_cid' => 'NULL',
+                                               'ar_user_text' => 'ar_user_text',
+                                               'ar_user' => 'ar_user',
+                                               'ar_actor' => 'NULL',
+                                               'ar_content_format',
+                                               'ar_content_model',
+                                       ]
+                               ),
+                               'joins' => [],
+                       ]
+               ];
+       }
+
+       public function provideGetQueryInfo() {
+               yield [
+                       [],
+                       [
+                               'tables' => [ 'revision' ],
+                               'fields' => array_merge(
+                                       $this->getDefaultQueryFields(),
+                                       $this->getCommentQueryFields(),
+                                       $this->getActorQueryFields(),
+                                       $this->getContentHandlerQueryFields()
+                               ),
+                               'joins' => [],
+                       ]
+               ];
+               yield [
+                       [ 'page', 'user' ],
+                       [
+                               'tables' => [ 'revision', 'page', 'user' ],
+                               'fields' => array_merge(
+                                       $this->getDefaultQueryFields(),
+                                       $this->getCommentQueryFields(),
+                                       $this->getActorQueryFields(),
+                                       $this->getContentHandlerQueryFields(),
+                                       [
+                                               'page_namespace',
+                                               'page_title',
+                                               'page_id',
+                                               'page_latest',
+                                               'page_is_redirect',
+                                               'page_len',
+                                               'user_name',
+                                       ]
+                               ),
+                               'joins' => [
+                                       'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
+                                       'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
+                               ],
+                       ]
+               ];
+       }
+
+       public function provideGetSlotsQueryInfo() {
+               $db = wfGetDB( DB_REPLICA );
+
+               yield [
+                       [],
+                       [
+                               'tables' => [
+                                       'slots' => 'revision',
+                               ],
+                               'fields' => array_merge(
+                                       [
+                                               'slot_revision_id' => 'slots.rev_id',
+                                               'slot_content_id' => 'NULL',
+                                               'slot_origin' => 'slots.rev_id',
+                                               'role_name' => $db->addQuotes( 'main' ),
+                                       ]
+                               ),
+                               'joins' => [],
+                       ]
+               ];
+               yield [
+                       [ 'content' ],
+                       [
+                               'tables' => [
+                                       'slots' => 'revision',
+                               ],
+                               'fields' => array_merge(
+                                       [
+                                               'slot_revision_id' => 'slots.rev_id',
+                                               'slot_content_id' => 'NULL',
+                                               'slot_origin' => 'slots.rev_id',
+                                               'role_name' => $db->addQuotes( 'main' ),
+                                               'content_size' => 'slots.rev_len',
+                                               'content_sha1' => 'slots.rev_sha1',
+                                               'content_address' =>
+                                                       'CONCAT(' . $db->addQuotes( 'tt:' ) . ',slots.rev_text_id)',
+                                               'model_name' => 'slots.rev_content_model',
+                                       ]
+                               ),
+                               'joins' => [],
+                       ]
+               ];
+       }
+
+       public function provideInsertRevisionOn_failures() {
+               foreach ( parent::provideInsertRevisionOn_failures() as $case ) {
+                       yield $case;
+               }
+
+               yield 'slot that is not main slot' => [
+                       [
+                               'content' => [
+                                       'main' => new WikitextContent( 'Chicken' ),
+                                       'lalala' => new WikitextContent( 'Duck' ),
+                               ],
+                               'comment' => $this->getRandomCommentStoreComment(),
+                               'timestamp' => '20171117010101',
+                               'user' => true,
+                       ],
+                       new InvalidArgumentException( 'Only the main slot is supported' )
+               ];
+       }
+
+       public function provideNewMutableRevisionFromArray() {
+               foreach ( parent::provideNewMutableRevisionFromArray() as $case ) {
+                       yield $case;
+               }
+
+               yield 'Basic array, with page & id' => [
+                       [
+                               'id' => 2,
+                               'page' => 1,
+                               'text_id' => 2,
+                               'timestamp' => '20171017114835',
+                               'user_text' => '111.0.1.2',
+                               'user' => 0,
+                               'minor_edit' => false,
+                               'deleted' => 0,
+                               'len' => 46,
+                               'parent_id' => 1,
+                               'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
+                               'comment' => 'Goat Comment!',
+                               'content_format' => 'text/x-wiki',
+                               'content_model' => 'wikitext',
+                       ]
+               ];
+       }
+
+}
diff --git a/tests/phpunit/includes/Storage/McrWriteBothSchemaOverride.php b/tests/phpunit/includes/Storage/McrWriteBothSchemaOverride.php
new file mode 100644 (file)
index 0000000..2a54dbe
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+namespace MediaWiki\Tests\Storage;
+
+use Wikimedia\Rdbms\IMaintainableDatabase;
+use MediaWiki\DB\PatchFileLocation;
+
+/**
+ * Trait providing schema overrides that allow tests to run against the intermediate MCR database
+ * schema for use during schema migration.
+ */
+trait McrWriteBothSchemaOverride {
+
+       use PatchFileLocation;
+       use McrSchemaDetection;
+
+       /**
+        * @return int
+        */
+       protected function getMcrMigrationStage() {
+               return MIGRATION_WRITE_BOTH;
+       }
+
+       /**
+        * @return string[]
+        */
+       protected function getMcrTablesToReset() {
+               return [ 'content', 'content_models', 'slots', 'slot_roles' ];
+       }
+
+       /**
+        * @override MediaWikiTestCase::getSchemaOverrides
+        * @return array[]
+        */
+       protected function getSchemaOverrides( IMaintainableDatabase $db ) {
+               $overrides = [
+                       'scripts' => [],
+                       'drop' => [],
+                       'create' => [],
+                       'alter' => [],
+               ];
+
+               if ( !$this->hasMcrTables( $db ) ) {
+                       $overrides['create'] = [ 'slots', 'content', 'slot_roles', 'content_models', ];
+                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slot_roles' );
+                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content_models' );
+                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-content' );
+                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'patch-slots' );
+               }
+
+               if ( !$this->hasPreMcrFields( $db ) ) {
+                       $overrides['alter'][] = 'revision';
+                       $overrides['scripts'][] = $this->getSqlPatchPath( $db, 'create-pre-mcr-fields', __DIR__ );
+               }
+
+               return $overrides;
+       }
+
+}
index dd2c4b6..62093f0 100644 (file)
@@ -7,6 +7,7 @@ use InvalidArgumentException;
 use MediaWiki\Storage\MutableRevisionRecord;
 use MediaWiki\Storage\RevisionAccessException;
 use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Storage\RevisionSlotsUpdate;
 use MediaWiki\Storage\SlotRecord;
 use MediaWiki\User\UserIdentityValue;
 use MediaWikiTestCase;
@@ -209,4 +210,82 @@ class MutableRevisionRecordTest extends MediaWikiTestCase {
                $this->assertSame( $comment, $record->getComment() );
        }
 
+       public function testSimpleGetOriginalAndInheritedSlots() {
+               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+               $mainSlot = new SlotRecord(
+                       (object)[
+                               'slot_id' => 1,
+                               'slot_revision_id' => null, // unsaved
+                               'slot_content_id' => 1,
+                               'content_address' => null, // touched
+                               'model_name' => 'x',
+                               'role_name' => 'main',
+                               'slot_origin' => null // touched
+                       ],
+                       new WikitextContent( 'main' )
+               );
+               $auxSlot = new SlotRecord(
+                       (object)[
+                               'slot_id' => 2,
+                               'slot_revision_id' => null, // unsaved
+                               'slot_content_id' => 1,
+                               'content_address' => 'foo', // inherited
+                               'model_name' => 'x',
+                               'role_name' => 'aux',
+                               'slot_origin' => 1 // inherited
+                       ],
+                       new WikitextContent( 'aux' )
+               );
+
+               $record->setSlot( $mainSlot );
+               $record->setSlot( $auxSlot );
+
+               $this->assertSame( [ 'main' ], $record->getOriginalSlots()->getSlotRoles() );
+               $this->assertSame( $mainSlot, $record->getOriginalSlots()->getSlot( 'main' ) );
+
+               $this->assertSame( [ 'aux' ], $record->getInheritedSlots()->getSlotRoles() );
+               $this->assertSame( $auxSlot, $record->getInheritedSlots()->getSlot( 'aux' ) );
+       }
+
+       public function testSimpleremoveSlot() {
+               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+
+               $a = new WikitextContent( 'a' );
+               $b = new WikitextContent( 'b' );
+
+               $record->inheritSlot( SlotRecord::newSaved( 7, 3, 'a', SlotRecord::newUnsaved( 'a', $a ) ) );
+               $record->inheritSlot( SlotRecord::newSaved( 7, 4, 'b', SlotRecord::newUnsaved( 'b', $b ) ) );
+
+               $record->removeSlot( 'b' );
+
+               $this->assertTrue( $record->hasSlot( 'a' ) );
+               $this->assertFalse( $record->hasSlot( 'b' ) );
+       }
+
+       public function testApplyUpdate() {
+               $update = new RevisionSlotsUpdate();
+
+               $a = new WikitextContent( 'a' );
+               $b = new WikitextContent( 'b' );
+               $c = new WikitextContent( 'c' );
+               $x = new WikitextContent( 'x' );
+
+               $update->modifyContent( 'b', $x );
+               $update->modifyContent( 'c', $x );
+               $update->removeSlot( 'c' );
+               $update->removeSlot( 'd' );
+
+               $record = new MutableRevisionRecord( Title::newFromText( 'Foo' ) );
+               $record->inheritSlot( SlotRecord::newSaved( 7, 3, 'a', SlotRecord::newUnsaved( 'a', $a ) ) );
+               $record->inheritSlot( SlotRecord::newSaved( 7, 4, 'b', SlotRecord::newUnsaved( 'b', $b ) ) );
+               $record->inheritSlot( SlotRecord::newSaved( 7, 5, 'c', SlotRecord::newUnsaved( 'c', $c ) ) );
+
+               $record->applyUpdate( $update );
+
+               $this->assertEquals( [ 'b' ], array_keys( $record->getOriginalSlots()->getSlots() ) );
+               $this->assertEquals( $a, $record->getSlot( 'a' )->getContent() );
+               $this->assertEquals( $x, $record->getSlot( 'b' )->getContent() );
+               $this->assertFalse( $record->hasSlot( 'c' ) );
+       }
+
 }
index f19be3b..5a83143 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace MediaWiki\Tests\Storage;
 
+use Content;
 use InvalidArgumentException;
 use MediaWiki\Storage\MutableRevisionSlots;
 use MediaWiki\Storage\RevisionAccessException;
@@ -75,6 +76,32 @@ class MutableRevisionSlotsTest extends RevisionSlotsTest {
                $this->assertSame( [ 'main' => $slotB ], $slots->getSlots() );
        }
 
+       /**
+        * @param string $role
+        * @param Content $content
+        * @return SlotRecord
+        */
+       private function newSavedSlot( $role, Content $content ) {
+               return SlotRecord::newSaved( 7, 7, 'xyz', SlotRecord::newUnsaved( $role, $content ) );
+       }
+
+       public function testInheritSlotOverwritesSlot() {
+               $slots = new MutableRevisionSlots();
+               $slotA = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) );
+               $slots->setSlot( $slotA );
+               $slotB = $this->newSavedSlot( 'main', new WikitextContent( 'B' ) );
+               $slotC = $this->newSavedSlot( 'foo', new WikitextContent( 'C' ) );
+               $slots->inheritSlot( $slotB );
+               $slots->inheritSlot( $slotC );
+               $this->assertSame( [ 'main', 'foo' ], $slots->getSlotRoles() );
+               $this->assertNotSame( $slotB, $slots->getSlot( 'main' ) );
+               $this->assertNotSame( $slotC, $slots->getSlot( 'foo' ) );
+               $this->assertTrue( $slots->getSlot( 'main' )->isInherited() );
+               $this->assertTrue( $slots->getSlot( 'foo' )->isInherited() );
+               $this->assertSame( $slotB->getContent(), $slots->getSlot( 'main' )->getContent() );
+               $this->assertSame( $slotC->getContent(), $slots->getSlot( 'foo' )->getContent() );
+       }
+
        public function testSetContentOfExistingSlotOverwritesContent() {
                $slots = new MutableRevisionSlots();
 
@@ -102,4 +129,20 @@ class MutableRevisionSlotsTest extends RevisionSlotsTest {
                $slots->getSlot( 'main' );
        }
 
+       public function testNewFromParentRevisionSlots() {
+               /** @var SlotRecord[] $parentSlots */
+               $parentSlots = [
+                       'some' => $this->newSavedSlot( 'some', new WikitextContent( 'X' ) ),
+                       'other' => $this->newSavedSlot( 'other', new WikitextContent( 'Y' ) ),
+               ];
+               $slots = MutableRevisionSlots::newFromParentRevisionSlots( $parentSlots );
+               $this->assertSame( [ 'some', 'other' ], $slots->getSlotRoles() );
+               $this->assertNotSame( $parentSlots['some'], $slots->getSlot( 'some' ) );
+               $this->assertNotSame( $parentSlots['other'], $slots->getSlot( 'other' ) );
+               $this->assertTrue( $slots->getSlot( 'some' )->isInherited() );
+               $this->assertTrue( $slots->getSlot( 'other' )->isInherited() );
+               $this->assertSame( $parentSlots['some']->getContent(), $slots->getContent( 'some' ) );
+               $this->assertSame( $parentSlots['other']->getContent(), $slots->getContent( 'other' ) );
+       }
+
 }
index 0cd164b..e5104d8 100644 (file)
@@ -83,14 +83,17 @@ class NameTableStoreTest extends MediaWikiTestCase {
                BagOStuff $cacheBag,
                $insertCalls,
                $selectCalls,
-               $normalizationCallback = null
+               $normalizationCallback = null,
+               $insertCallback = null
        ) {
                return new NameTableStore(
                        $this->getMockLoadBalancer( $this->getCallCheckingDb( $insertCalls, $selectCalls ) ),
                        $this->getHashWANObjectCache( $cacheBag ),
                        new NullLogger(),
                        'slot_roles', 'role_id', 'role_name',
-                       $normalizationCallback
+                       $normalizationCallback,
+                       false,
+                       $insertCallback
                );
        }
 
@@ -295,4 +298,18 @@ class NameTableStoreTest extends MediaWikiTestCase {
                $this->assertSame( $barId, $store3->acquireId( 'bar' ) );
        }
 
+       public function testGetAndAcquireIdInsertCallback() {
+               $store = $this->getNameTableSqlStore(
+                       new EmptyBagOStuff(),
+                       1,
+                       1,
+                       null,
+                       function ( $insertFields ) {
+                               $insertFields['role_id'] = 7251;
+                               return $insertFields;
+                       }
+               );
+               $this->assertSame( 7251, $store->acquireId( 'A' ) );
+       }
+
 }
index c77a94a..2337805 100644 (file)
@@ -1,6 +1,8 @@
 <?php
 namespace MediaWiki\Tests\Storage;
 
+use Revision;
+
 /**
  * Tests RevisionStore against the pre-MCR, pre-ContentHandler DB schema.
  *
@@ -19,6 +21,14 @@ class NoContentModelRevisionStoreDbTest extends RevisionStoreDbTestBase {
                return false;
        }
 
+       protected function revisionToRow( Revision $rev, $options = [ 'page', 'user', 'comment' ] ) {
+               $row = parent::revisionToRow( $rev, $options );
+
+               $row->rev_text_id = (string)$rev->getTextId();
+
+               return $row;
+       }
+
        public function provideGetArchiveQueryInfo() {
                yield [
                        [
@@ -111,4 +121,71 @@ class NoContentModelRevisionStoreDbTest extends RevisionStoreDbTestBase {
                ];
        }
 
+       public function provideGetSlotsQueryInfo() {
+               $db = wfGetDB( DB_REPLICA );
+
+               yield [
+                       [],
+                       [
+                               'tables' => [
+                                       'slots' => 'revision',
+                               ],
+                               'fields' => array_merge(
+                                       [
+                                               'slot_revision_id' => 'slots.rev_id',
+                                               'slot_content_id' => 'NULL',
+                                               'slot_origin' => 'slots.rev_id',
+                                               'role_name' => $db->addQuotes( 'main' ),
+                                       ]
+                               ),
+                               'joins' => [],
+                       ]
+               ];
+               yield [
+                       [ 'content' ],
+                       [
+                               'tables' => [
+                                       'slots' => 'revision',
+                               ],
+                               'fields' => array_merge(
+                                       [
+                                               'slot_revision_id' => 'slots.rev_id',
+                                               'slot_content_id' => 'NULL',
+                                               'slot_origin' => 'slots.rev_id',
+                                               'role_name' => $db->addQuotes( 'main' ),
+                                               'content_size' => 'slots.rev_len',
+                                               'content_sha1' => 'slots.rev_sha1',
+                                               'content_address' =>
+                                                       'CONCAT(' . $db->addQuotes( 'tt:' ) . ',slots.rev_text_id)',
+                                               'model_name' => 'NULL',
+                                       ]
+                               ),
+                               'joins' => [],
+                       ]
+               ];
+       }
+
+       public function provideNewMutableRevisionFromArray() {
+               foreach ( parent::provideNewMutableRevisionFromArray() as $case ) {
+                       yield $case;
+               }
+
+               yield 'Basic array, with page & id' => [
+                       [
+                               'id' => 2,
+                               'page' => 1,
+                               'text_id' => 2,
+                               'timestamp' => '20171017114835',
+                               'user_text' => '111.0.1.2',
+                               'user' => 0,
+                               'minor_edit' => false,
+                               'deleted' => 0,
+                               'len' => 46,
+                               'parent_id' => 1,
+                               'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
+                               'comment' => 'Goat Comment!',
+                       ]
+               ];
+       }
+
 }
diff --git a/tests/phpunit/includes/Storage/PageUpdaterTest.php b/tests/phpunit/includes/Storage/PageUpdaterTest.php
new file mode 100644 (file)
index 0000000..24107b1
--- /dev/null
@@ -0,0 +1,530 @@
+<?php
+
+namespace MediaWiki\Tests\Storage;
+
+use CommentStoreComment;
+use Content;
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Storage\RevisionRecord;
+use MediaWikiTestCase;
+use RecentChange;
+use Revision;
+use TextContent;
+use Title;
+use WikiPage;
+
+/**
+ * @covers \MediaWiki\Storage\PageUpdater
+ * @group Database
+ */
+class PageUpdaterTest extends MediaWikiTestCase {
+
+       private function getDummyTitle( $method ) {
+               return Title::newFromText( $method, $this->getDefaultWikitextNS() );
+       }
+
+       /**
+        * @param int $revId
+        *
+        * @return null|RecentChange
+        */
+       private function getRecentChangeFor( $revId ) {
+               $qi = RecentChange::getQueryInfo();
+               $row = $this->db->selectRow(
+                       $qi['tables'],
+                       $qi['fields'],
+                       [ 'rc_this_oldid' => $revId ],
+                       __METHOD__,
+                       [],
+                       $qi['joins']
+               );
+
+               return $row ? RecentChange::newFromRow( $row ) : null;
+       }
+
+       // TODO: test setAjaxEditStash();
+
+       /**
+        * @covers \MediaWiki\Storage\PageUpdater::saveRevision()
+        * @covers \WikiPage::newPageUpdater()
+        */
+       public function testCreatePage() {
+               $user = $this->getTestUser()->getUser();
+
+               $title = $this->getDummyTitle( __METHOD__ );
+               $page = WikiPage::factory( $title );
+               $updater = $page->newPageUpdater( $user );
+
+               $oldStats = $this->db->selectRow( 'site_stats', '*', '1=1' );
+
+               $this->assertFalse( $updater->wasCommitted(), 'wasCommitted' );
+               $this->assertFalse( $updater->getBaseRevisionId(), 'getBaseRevisionId' );
+               $this->assertSame( 0, $updater->getUndidRevisionId(), 'getUndidRevisionId' );
+
+               $updater->setBaseRevisionId( 0 );
+               $this->assertSame( 0, $updater->getBaseRevisionId(), 'getBaseRevisionId' );
+
+               $updater->addTag( 'foo' );
+               $updater->addTags( [ 'bar', 'qux' ] );
+
+               $tags = $updater->getExplicitTags();
+               sort( $tags );
+               $this->assertSame( [ 'bar', 'foo', 'qux' ], $tags, 'getExplicitTags' );
+
+               // TODO: MCR: test additional slots
+               $content = new TextContent( 'Lorem Ipsum' );
+               $updater->setContent( 'main', $content );
+
+               $parent = $updater->grabParentRevision();
+
+               // TODO: test that hasEditConflict() grabs the parent revision
+               $this->assertNull( $parent, 'getParentRevision' );
+               $this->assertFalse( $updater->wasCommitted(), 'wasCommitted' );
+               $this->assertFalse( $updater->hasEditConflict(), 'hasEditConflict' );
+
+               // TODO: test failure with EDIT_UPDATE
+               // TODO: test EDIT_MINOR, EDIT_BOT, etc
+               $summary = CommentStoreComment::newUnsavedComment( 'Just a test' );
+               $rev = $updater->saveRevision( $summary );
+
+               $this->assertNotNull( $rev );
+               $this->assertSame( 0, $rev->getParentId() );
+               $this->assertSame( $summary->text, $rev->getComment( RevisionRecord::RAW )->text );
+               $this->assertSame( $user->getName(), $rev->getUser( RevisionRecord::RAW )->getName() );
+
+               $this->assertTrue( $updater->wasCommitted(), 'wasCommitted()' );
+               $this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' );
+               $this->assertTrue( $updater->getStatus()->isOK(), 'getStatus()->isOK()' );
+               $this->assertTrue( $updater->isNew(), 'isNew()' );
+               $this->assertFalse( $updater->isUnchanged(), 'isUnchanged()' );
+               $this->assertNotNull( $updater->getNewRevision(), 'getNewRevision()' );
+               $this->assertInstanceOf( Revision::class, $updater->getStatus()->value['revision'] );
+
+               $rev = $updater->getNewRevision();
+               $revContent = $rev->getContent( 'main' );
+               $this->assertSame( 'Lorem Ipsum', $revContent->serialize(), 'revision content' );
+
+               // were the WikiPage and Title objects updated?
+               $this->assertTrue( $page->exists(), 'WikiPage::exists()' );
+               $this->assertTrue( $title->exists(), 'Title::exists()' );
+               $this->assertSame( $rev->getId(), $page->getLatest(), 'WikiPage::getRevision()' );
+               $this->assertNotNull( $page->getRevision(), 'WikiPage::getRevision()' );
+
+               // re-load
+               $page2 = WikiPage::factory( $title );
+               $this->assertTrue( $page2->exists(), 'WikiPage::exists()' );
+               $this->assertSame( $rev->getId(), $page2->getLatest(), 'WikiPage::getRevision()' );
+               $this->assertNotNull( $page2->getRevision(), 'WikiPage::getRevision()' );
+
+               // Check RC entry
+               $rc = $this->getRecentChangeFor( $rev->getId() );
+               $this->assertNotNull( $rc, 'RecentChange' );
+
+               // check site stats - this asserts that derived data updates where run.
+               $stats = $this->db->selectRow( 'site_stats', '*', '1=1' );
+               $this->assertSame( $oldStats->ss_total_pages + 1, (int)$stats->ss_total_pages );
+               $this->assertSame( $oldStats->ss_total_edits + 1, (int)$stats->ss_total_edits );
+
+               // re-edit with same content - should be a "null-edit"
+               $updater = $page->newPageUpdater( $user );
+               $updater->setContent( 'main', $content );
+
+               $summary = CommentStoreComment::newUnsavedComment( 'to to re-edit' );
+               $rev = $updater->saveRevision( $summary );
+               $status = $updater->getStatus();
+
+               $this->assertNull( $rev, 'getNewRevision()' );
+               $this->assertNull( $updater->getNewRevision(), 'getNewRevision()' );
+               $this->assertTrue( $updater->isUnchanged(), 'isUnchanged' );
+               $this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' );
+               $this->assertTrue( $status->isOK(), 'getStatus()->isOK()' );
+               $this->assertTrue( $status->hasMessage( 'edit-no-change' ), 'edit-no-change' );
+       }
+
+       /**
+        * @covers \MediaWiki\Storage\PageUpdater::saveRevision()
+        * @covers \WikiPage::newPageUpdater()
+        */
+       public function testUpdatePage() {
+               $user = $this->getTestUser()->getUser();
+
+               $title = $this->getDummyTitle( __METHOD__ );
+               $this->insertPage( $title );
+
+               $page = WikiPage::factory( $title );
+               $parentId = $page->getLatest();
+
+               $updater = $page->newPageUpdater( $user );
+
+               $oldStats = $this->db->selectRow( 'site_stats', '*', '1=1' );
+
+               // TODO: test page update does not fail with mismatching base rev ID
+               $baseRev = $title->getLatestRevID( Title::GAID_FOR_UPDATE );
+               $updater->setBaseRevisionId( $baseRev );
+               $this->assertSame( $baseRev, $updater->getBaseRevisionId(), 'getBaseRevisionId' );
+
+               // TODO: MCR: test additional slots
+               $updater->setContent( 'main', new TextContent( 'Lorem Ipsum' ) );
+
+               // TODO: test all flags for saveRevision()!
+               $summary = CommentStoreComment::newUnsavedComment( 'Just a test' );
+               $rev = $updater->saveRevision( $summary );
+
+               $this->assertNotNull( $rev );
+               $this->assertSame( $parentId, $rev->getParentId() );
+               $this->assertSame( $summary->text, $rev->getComment( RevisionRecord::RAW )->text );
+               $this->assertSame( $user->getName(), $rev->getUser( RevisionRecord::RAW )->getName() );
+
+               $this->assertTrue( $updater->wasCommitted(), 'wasCommitted()' );
+               $this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' );
+               $this->assertTrue( $updater->getStatus()->isOK(), 'getStatus()->isOK()' );
+               $this->assertFalse( $updater->isNew(), 'isNew()' );
+               $this->assertNotNull( $updater->getNewRevision(), 'getNewRevision()' );
+               $this->assertInstanceOf( Revision::class, $updater->getStatus()->value['revision'] );
+               $this->assertFalse( $updater->isUnchanged(), 'isUnchanged()' );
+
+               // TODO: Test null revision (with different user): new revision!
+
+               $rev = $updater->getNewRevision();
+               $revContent = $rev->getContent( 'main' );
+               $this->assertSame( 'Lorem Ipsum', $revContent->serialize(), 'revision content' );
+
+               // were the WikiPage and Title objects updated?
+               $this->assertTrue( $page->exists(), 'WikiPage::exists()' );
+               $this->assertTrue( $title->exists(), 'Title::exists()' );
+               $this->assertSame( $rev->getId(), $page->getLatest(), 'WikiPage::getRevision()' );
+               $this->assertNotNull( $page->getRevision(), 'WikiPage::getRevision()' );
+
+               // re-load
+               $page2 = WikiPage::factory( $title );
+               $this->assertTrue( $page2->exists(), 'WikiPage::exists()' );
+               $this->assertSame( $rev->getId(), $page2->getLatest(), 'WikiPage::getRevision()' );
+               $this->assertNotNull( $page2->getRevision(), 'WikiPage::getRevision()' );
+
+               // Check RC entry
+               $rc = $this->getRecentChangeFor( $rev->getId() );
+               $this->assertNotNull( $rc, 'RecentChange' );
+
+               // re-edit
+               $updater = $page->newPageUpdater( $user );
+               $updater->setContent( 'main', new TextContent( 'dolor sit amet' ) );
+
+               $summary = CommentStoreComment::newUnsavedComment( 're-edit' );
+               $updater->saveRevision( $summary );
+               $this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' );
+               $this->assertTrue( $updater->getStatus()->isOK(), 'getStatus()->isOK()' );
+
+               // check site stats - this asserts that derived data updates where run.
+               $stats = $this->db->selectRow( 'site_stats', '*', '1=1' );
+               $this->assertSame( $oldStats->ss_total_pages + 0, (int)$stats->ss_total_pages );
+               $this->assertSame( $oldStats->ss_total_edits + 2, (int)$stats->ss_total_edits );
+       }
+
+       /**
+        * Creates a revision in the database.
+        *
+        * @param WikiPage $page
+        * @param $summary
+        * @param null|string|Content $content
+        *
+        * @return RevisionRecord|null
+        */
+       private function createRevision( WikiPage $page, $summary, $content = null ) {
+               $user = $this->getTestUser()->getUser();
+               $comment = CommentStoreComment::newUnsavedComment( $summary );
+
+               if ( !$content instanceof Content ) {
+                       $content = new TextContent( $content === null ? $summary : $content );
+               }
+
+               $updater = $page->newPageUpdater( $user );
+               $updater->setContent( 'main', $content );
+               $rev = $updater->saveRevision( $comment );
+               return $rev;
+       }
+
+       /**
+        * @covers \MediaWiki\Storage\PageUpdater::grabParentRevision()
+        * @covers \MediaWiki\Storage\PageUpdater::saveRevision()
+        */
+       public function testCompareAndSwapFailure() {
+               $user = $this->getTestUser()->getUser();
+
+               $title = $this->getDummyTitle( __METHOD__ );
+
+               // start editing non-existing page
+               $page = WikiPage::factory( $title );
+               $updater = $page->newPageUpdater( $user );
+               $updater->grabParentRevision();
+
+               // create page concurrently
+               $concurrentPage = WikiPage::factory( $title );
+               $this->createRevision( $concurrentPage, __METHOD__ . '-one' );
+
+               // try creating the page - should trigger CAS failure.
+               $summary = CommentStoreComment::newUnsavedComment( 'create?!' );
+               $updater->setContent( 'main', new TextContent( 'Lorem ipsum' ) );
+               $updater->saveRevision( $summary );
+               $status = $updater->getStatus();
+
+               $this->assertFalse( $updater->wasSuccessful(), 'wasSuccessful()' );
+               $this->assertNull( $updater->getNewRevision(), 'getNewRevision()' );
+               $this->assertFalse( $status->isOK(), 'getStatus()->isOK()' );
+               $this->assertTrue( $status->hasMessage( 'edit-already-exists' ), 'edit-conflict' );
+
+               // start editing existing page
+               $page = WikiPage::factory( $title );
+               $updater = $page->newPageUpdater( $user );
+               $updater->grabParentRevision();
+
+               // update page concurrently
+               $concurrentPage = WikiPage::factory( $title );
+               $this->createRevision( $concurrentPage, __METHOD__ . '-two' );
+
+               // try creating the page - should trigger CAS failure.
+               $summary = CommentStoreComment::newUnsavedComment( 'edit?!' );
+               $updater->setContent( 'main', new TextContent( 'dolor sit amet' ) );
+               $updater->saveRevision( $summary );
+               $status = $updater->getStatus();
+
+               $this->assertFalse( $updater->wasSuccessful(), 'wasSuccessful()' );
+               $this->assertNull( $updater->getNewRevision(), 'getNewRevision()' );
+               $this->assertFalse( $status->isOK(), 'getStatus()->isOK()' );
+               $this->assertTrue( $status->hasMessage( 'edit-conflict' ), 'edit-conflict' );
+       }
+
+       /**
+        * @covers \MediaWiki\Storage\PageUpdater::saveRevision()
+        */
+       public function testFailureOnEditFlags() {
+               $user = $this->getTestUser()->getUser();
+
+               $title = $this->getDummyTitle( __METHOD__ );
+
+               // start editing non-existing page
+               $page = WikiPage::factory( $title );
+               $updater = $page->newPageUpdater( $user );
+
+               // update with EDIT_UPDATE flag should fail
+               $summary = CommentStoreComment::newUnsavedComment( 'udpate?!' );
+               $updater->setContent( 'main', new TextContent( 'Lorem ipsum' ) );
+               $updater->saveRevision( $summary, EDIT_UPDATE );
+               $status = $updater->getStatus();
+
+               $this->assertFalse( $updater->wasSuccessful(), 'wasSuccessful()' );
+               $this->assertNull( $updater->getNewRevision(), 'getNewRevision()' );
+               $this->assertFalse( $status->isOK(), 'getStatus()->isOK()' );
+               $this->assertTrue( $status->hasMessage( 'edit-gone-missing' ), 'edit-gone-missing' );
+
+               // create the page
+               $this->createRevision( $page, __METHOD__ );
+
+               // update with EDIT_NEW flag should fail
+               $summary = CommentStoreComment::newUnsavedComment( 'create?!' );
+               $updater = $page->newPageUpdater( $user );
+               $updater->setContent( 'main', new TextContent( 'dolor sit amet' ) );
+               $updater->saveRevision( $summary, EDIT_NEW );
+               $status = $updater->getStatus();
+
+               $this->assertFalse( $updater->wasSuccessful(), 'wasSuccessful()' );
+               $this->assertNull( $updater->getNewRevision(), 'getNewRevision()' );
+               $this->assertFalse( $status->isOK(), 'getStatus()->isOK()' );
+               $this->assertTrue( $status->hasMessage( 'edit-already-exists' ), 'edit-already-exists' );
+       }
+
+       /**
+        * @covers \MediaWiki\Storage\PageUpdater::saveRevision()
+        * @covers \MediaWiki\Storage\PageUpdater::setBaseRevisionId()
+        */
+       public function testFailureOnBaseRevision() {
+               $user = $this->getTestUser()->getUser();
+
+               $title = $this->getDummyTitle( __METHOD__ );
+
+               // start editing non-existing page
+               $page = WikiPage::factory( $title );
+               $updater = $page->newPageUpdater( $user );
+
+               // update for base revision 7 should fail
+               $summary = CommentStoreComment::newUnsavedComment( 'udpate?!' );
+               $updater->setBaseRevisionId( 7 ); // expect page to exist
+               $updater->setContent( 'main', new TextContent( 'Lorem ipsum' ) );
+               $updater->saveRevision( $summary );
+               $status = $updater->getStatus();
+
+               $this->assertFalse( $updater->wasSuccessful(), 'wasSuccessful()' );
+               $this->assertNull( $updater->getNewRevision(), 'getNewRevision()' );
+               $this->assertFalse( $status->isOK(), 'getStatus()->isOK()' );
+               $this->assertTrue( $status->hasMessage( 'edit-gone-missing' ), 'edit-gone-missing' );
+
+               // create the page
+               $this->createRevision( $page, __METHOD__ );
+
+               // update for base revision 0 should fail
+               $summary = CommentStoreComment::newUnsavedComment( 'create?!' );
+               $updater = $page->newPageUpdater( $user );
+               $updater->setBaseRevisionId( 0 ); // expect page to not exist
+               $updater->setContent( 'main', new TextContent( 'dolor sit amet' ) );
+               $updater->saveRevision( $summary );
+               $status = $updater->getStatus();
+
+               $this->assertFalse( $updater->wasSuccessful(), 'wasSuccessful()' );
+               $this->assertNull( $updater->getNewRevision(), 'getNewRevision()' );
+               $this->assertFalse( $status->isOK(), 'getStatus()->isOK()' );
+               $this->assertTrue( $status->hasMessage( 'edit-already-exists' ), 'edit-already-exists' );
+       }
+
+       public function provideSetRcPatrolStatus( $patrolled ) {
+               yield [ RecentChange::PRC_UNPATROLLED ];
+               yield [ RecentChange::PRC_AUTOPATROLLED ];
+       }
+
+       /**
+        * @dataProvider provideSetRcPatrolStatus
+        * @covers \MediaWiki\Storage\PageUpdater::setRcPatrolStatus()
+        */
+       public function testSetRcPatrolStatus( $patrolled ) {
+               $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
+
+               $user = $this->getTestUser()->getUser();
+
+               $title = $this->getDummyTitle( __METHOD__ );
+
+               $page = WikiPage::factory( $title );
+               $updater = $page->newPageUpdater( $user );
+
+               $summary = CommentStoreComment::newUnsavedComment( 'Lorem ipsum ' . $patrolled );
+               $updater->setContent( 'main', new TextContent( 'Lorem ipsum ' . $patrolled ) );
+               $updater->setRcPatrolStatus( $patrolled );
+               $rev = $updater->saveRevision( $summary );
+
+               $rc = $revisionStore->getRecentChange( $rev );
+               $this->assertEquals( $patrolled, $rc->getAttribute( 'rc_patrolled' ) );
+       }
+
+       /**
+        * @covers \MediaWiki\Storage\PageUpdater::inheritSlot()
+        * @covers \MediaWiki\Storage\PageUpdater::setContent()
+        */
+       public function testInheritSlot() {
+               $user = $this->getTestUser()->getUser();
+               $title = $this->getDummyTitle( __METHOD__ );
+               $page = WikiPage::factory( $title );
+
+               $updater = $page->newPageUpdater( $user );
+               $summary = CommentStoreComment::newUnsavedComment( 'one' );
+               $updater->setContent( 'main', new TextContent( 'Lorem ipsum' ) );
+               $rev1 = $updater->saveRevision( $summary, EDIT_NEW );
+
+               $updater = $page->newPageUpdater( $user );
+               $summary = CommentStoreComment::newUnsavedComment( 'two' );
+               $updater->setContent( 'main', new TextContent( 'Foo Bar' ) );
+               $rev2 = $updater->saveRevision( $summary, EDIT_UPDATE );
+
+               $updater = $page->newPageUpdater( $user );
+               $summary = CommentStoreComment::newUnsavedComment( 'three' );
+               $updater->inheritSlot( $rev1->getSlot( 'main' ) );
+               $rev3 = $updater->saveRevision( $summary, EDIT_UPDATE );
+
+               $this->assertNotSame( $rev1->getId(), $rev3->getId() );
+               $this->assertNotSame( $rev2->getId(), $rev3->getId() );
+
+               $main1 = $rev1->getSlot( 'main' );
+               $main3 = $rev3->getSlot( 'main' );
+
+               $this->assertNotSame( $main1->getRevision(), $main3->getRevision() );
+               $this->assertSame( $main1->getAddress(), $main3->getAddress() );
+               $this->assertTrue( $main1->getContent()->equals( $main3->getContent() ) );
+       }
+
+       // TODO: MCR: test adding multiple slots, inheriting parent slots, and removing slots.
+
+       public function testSetUseAutomaticEditSummaries() {
+               $this->setContentLang( 'qqx' );
+               $user = $this->getTestUser()->getUser();
+
+               $title = $this->getDummyTitle( __METHOD__ );
+               $page = WikiPage::factory( $title );
+
+               $updater = $page->newPageUpdater( $user );
+               $updater->setUseAutomaticEditSummaries( true );
+               $updater->setContent( 'main', new TextContent( 'Lorem Ipsum' ) );
+
+               // empty comment triggers auto-summary
+               $summary = CommentStoreComment::newUnsavedComment( '' );
+               $updater->saveRevision( $summary, EDIT_AUTOSUMMARY );
+
+               $rev = $updater->getNewRevision();
+               $comment = $rev->getComment( RevisionRecord::RAW );
+               $this->assertSame( '(autosumm-new: Lorem Ipsum)', $comment->text, 'comment text' );
+
+               // check that this also works when blanking the page
+               $updater = $page->newPageUpdater( $user );
+               $updater->setUseAutomaticEditSummaries( true );
+               $updater->setContent( 'main', new TextContent( '' ) );
+
+               $summary = CommentStoreComment::newUnsavedComment( '' );
+               $updater->saveRevision( $summary, EDIT_AUTOSUMMARY );
+
+               $rev = $updater->getNewRevision();
+               $comment = $rev->getComment( RevisionRecord::RAW );
+               $this->assertSame( '(autosumm-blank)', $comment->text, 'comment text' );
+
+               // check that we can also disable edit-summaries
+               $title2 = $this->getDummyTitle( __METHOD__ . '/2' );
+               $page2 = WikiPage::factory( $title2 );
+
+               $updater = $page2->newPageUpdater( $user );
+               $updater->setUseAutomaticEditSummaries( false );
+               $updater->setContent( 'main', new TextContent( 'Lorem Ipsum' ) );
+
+               $summary = CommentStoreComment::newUnsavedComment( '' );
+               $updater->saveRevision( $summary, EDIT_AUTOSUMMARY );
+
+               $rev = $updater->getNewRevision();
+               $comment = $rev->getComment( RevisionRecord::RAW );
+               $this->assertSame( '', $comment->text, 'comment text should still be lank' );
+
+               // check that we don't do auto.summaries without the EDIT_AUTOSUMMARY flag
+               $updater = $page2->newPageUpdater( $user );
+               $updater->setUseAutomaticEditSummaries( true );
+               $updater->setContent( 'main', new TextContent( '' ) );
+
+               $summary = CommentStoreComment::newUnsavedComment( '' );
+               $updater->saveRevision( $summary, 0 );
+
+               $rev = $updater->getNewRevision();
+               $comment = $rev->getComment( RevisionRecord::RAW );
+               $this->assertSame( '', $comment->text, 'comment text' );
+       }
+
+       public function provideSetUsePageCreationLog() {
+               yield [ true, [ [ 'create', 'create' ] ] ];
+               yield [ false, [] ];
+       }
+
+       /**
+        * @dataProvider provideSetUsePageCreationLog
+        * @param bool $use
+        */
+       public function testSetUsePageCreationLog( $use, $expected ) {
+               $user = $this->getTestUser()->getUser();
+               $title = $this->getDummyTitle( __METHOD__ . ( $use ? '_logged' : '_unlogged' ) );
+               $page = WikiPage::factory( $title );
+
+               $updater = $page->newPageUpdater( $user );
+               $updater->setUsePageCreationLog( $use );
+               $summary = CommentStoreComment::newUnsavedComment( 'cmt' );
+               $updater->setContent( 'main', new TextContent( 'Lorem Ipsum' ) );
+               $updater->saveRevision( $summary, EDIT_NEW );
+
+               $rev = $updater->getNewRevision();
+               $this->assertSelect(
+                       'logging',
+                       [ 'log_type', 'log_action' ],
+                       [ 'log_page' => $rev->getPageId() ],
+                       $expected
+               );
+       }
+
+}
index 4336691..a27d2bb 100644 (file)
@@ -1,6 +1,10 @@
 <?php
 namespace MediaWiki\Tests\Storage;
 
+use InvalidArgumentException;
+use Revision;
+use WikitextContent;
+
 /**
  * Tests RevisionStore against the pre-MCR DB schema.
  *
@@ -15,6 +19,16 @@ class PreMcrRevisionStoreDbTest extends RevisionStoreDbTestBase {
 
        use PreMcrSchemaOverride;
 
+       protected function revisionToRow( Revision $rev, $options = [ 'page', 'user', 'comment' ] ) {
+               $row = parent::revisionToRow( $rev, $options );
+
+               $row->rev_text_id = (string)$rev->getTextId();
+               $row->rev_content_format = (string)$rev->getContentFormat();
+               $row->rev_content_model = (string)$rev->getContentModel();
+
+               return $row;
+       }
+
        public function provideGetArchiveQueryInfo() {
                yield [
                        [
@@ -81,4 +95,92 @@ class PreMcrRevisionStoreDbTest extends RevisionStoreDbTestBase {
                ];
        }
 
+       public function provideGetSlotsQueryInfo() {
+               $db = wfGetDB( DB_REPLICA );
+
+               yield [
+                       [],
+                       [
+                               'tables' => [
+                                       'slots' => 'revision',
+                               ],
+                               'fields' => array_merge(
+                                       [
+                                               'slot_revision_id' => 'slots.rev_id',
+                                               'slot_content_id' => 'NULL',
+                                               'slot_origin' => 'slots.rev_id',
+                                               'role_name' => $db->addQuotes( 'main' ),
+                                       ]
+                               ),
+                               'joins' => [],
+                       ]
+               ];
+               yield [
+                       [ 'content' ],
+                       [
+                               'tables' => [
+                                       'slots' => 'revision',
+                               ],
+                               'fields' => array_merge(
+                                       [
+                                               'slot_revision_id' => 'slots.rev_id',
+                                               'slot_content_id' => 'NULL',
+                                               'slot_origin' => 'slots.rev_id',
+                                               'role_name' => $db->addQuotes( 'main' ),
+                                               'content_size' => 'slots.rev_len',
+                                               'content_sha1' => 'slots.rev_sha1',
+                                               'content_address' =>
+                                                       'CONCAT(' . $db->addQuotes( 'tt:' ) . ',slots.rev_text_id)',
+                                               'model_name' => 'slots.rev_content_model',
+                                       ]
+                               ),
+                               'joins' => [],
+                       ]
+               ];
+       }
+
+       public function provideInsertRevisionOn_failures() {
+               foreach ( parent::provideInsertRevisionOn_failures() as $case ) {
+                       yield $case;
+               }
+
+               yield 'slot that is not main slot' => [
+                       [
+                               'content' => [
+                                       'main' => new WikitextContent( 'Chicken' ),
+                                       'lalala' => new WikitextContent( 'Duck' ),
+                               ],
+                               'comment' => $this->getRandomCommentStoreComment(),
+                               'timestamp' => '20171117010101',
+                               'user' => true,
+                       ],
+                       new InvalidArgumentException( 'Only the main slot is supported' )
+               ];
+       }
+
+       public function provideNewMutableRevisionFromArray() {
+               foreach ( parent::provideNewMutableRevisionFromArray() as $case ) {
+                       yield $case;
+               }
+
+               yield 'Basic array, with page & id' => [
+                       [
+                               'id' => 2,
+                               'page' => 1,
+                               'text_id' => 2,
+                               'timestamp' => '20171017114835',
+                               'user_text' => '111.0.1.2',
+                               'user' => 0,
+                               'minor_edit' => false,
+                               'deleted' => 0,
+                               'len' => 46,
+                               'parent_id' => 1,
+                               'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
+                               'comment' => 'Goat Comment!',
+                               'content_format' => 'text/x-wiki',
+                               'content_model' => 'wikitext',
+                       ]
+               ];
+       }
+
 }
index 95bba47..ef14a9e 100644 (file)
@@ -136,9 +136,9 @@ class RevisionSlotsTest extends MediaWikiTestCase {
        }
 
        /**
-        * @covers \MediaWiki\Storage\RevisionSlots::getTouchedSlots
+        * @covers \MediaWiki\Storage\RevisionSlots::getOriginalSlots
         */
-       public function testGetTouchedSlots() {
+       public function testGetOriginalSlots() {
                $mainSlot = SlotRecord::newUnsaved( 'main', new WikitextContent( 'A' ) );
                $auxSlot = SlotRecord::newInherited(
                        SlotRecord::newSaved(
@@ -149,7 +149,7 @@ class RevisionSlotsTest extends MediaWikiTestCase {
                $slotsArray = [ $mainSlot, $auxSlot ];
                $slots = $this->newRevisionSlots( $slotsArray );
 
-               $this->assertEquals( [ 'main' => $mainSlot ], $slots->getTouchedSlots() );
+               $this->assertEquals( [ 'main' => $mainSlot ], $slots->getOriginalSlots() );
        }
 
        public function provideComputeSize() {
index 5b392c8..07a6971 100644 (file)
@@ -2,6 +2,8 @@
 
 namespace MediaWiki\Tests\Storage;
 
+use Content;
+use MediaWiki\Storage\MutableRevisionSlots;
 use MediaWiki\Storage\RevisionSlots;
 use MediaWiki\Storage\RevisionSlotsUpdate;
 use MediaWiki\Storage\RevisionAccessException;
@@ -41,8 +43,8 @@ class RevisionSlotsUpdateTest extends MediaWikiTestCase {
         *
         * @param RevisionSlots $newSlots
         * @param RevisionSlots $parentSlots
-        * @param $modified
-        * @param $removed
+        * @param string[] $modified
+        * @param string[] $removed
         */
        public function testNewFromRevisionSlots(
                RevisionSlots $newSlots,
@@ -60,6 +62,44 @@ class RevisionSlotsUpdateTest extends MediaWikiTestCase {
                }
        }
 
+       public function provideNewFromContent() {
+               $slotA = SlotRecord::newUnsaved( 'A', new WikitextContent( 'A' ) );
+               $slotB = SlotRecord::newUnsaved( 'B', new WikitextContent( 'B' ) );
+               $slotC = SlotRecord::newUnsaved( 'C', new WikitextContent( 'C' ) );
+
+               $parentSlots = new RevisionSlots( [
+                       'A' => $slotA,
+                       'B' => $slotB,
+                       'C' => $slotC,
+               ] );
+
+               $newContent = [
+                       'A' => new WikitextContent( 'A' ),
+                       'B' => new WikitextContent( 'B2' ),
+               ];
+
+               yield [ $newContent, null, [ 'A', 'B' ] ];
+               yield [ $newContent, $parentSlots, [ 'B' ] ];
+       }
+
+       /**
+        * @dataProvider provideNewFromContent
+        *
+        * @param Content[] $newContent
+        * @param RevisionSlots $parentSlots
+        * @param string[] $modified
+        */
+       public function testNewFromContent(
+               array $newContent,
+               RevisionSlots $parentSlots = null,
+               array $modified = []
+       ) {
+               $update = RevisionSlotsUpdate::newFromContent( $newContent, $parentSlots );
+
+               $this->assertEquals( $modified, $update->getModifiedRoles() );
+               $this->assertEmpty( $update->getRemovedRoles() );
+       }
+
        public function testConstructor() {
                $update = new RevisionSlotsUpdate();
 
@@ -204,4 +244,34 @@ class RevisionSlotsUpdateTest extends MediaWikiTestCase {
                $this->assertSame( $same, $b->hasSameUpdates( $a ) );
        }
 
+       /**
+        * @param string $role
+        * @param Content $content
+        * @return SlotRecord
+        */
+       private function newSavedSlot( $role, Content $content ) {
+               return SlotRecord::newSaved( 7, 7, 'xyz', SlotRecord::newUnsaved( $role, $content ) );
+       }
+
+       public function testApplyUpdate() {
+               /** @var SlotRecord[] $parentSlots */
+               $parentSlots = [
+                       'X' => $this->newSavedSlot( 'X', new WikitextContent( 'X' ) ),
+                       'Y' => $this->newSavedSlot( 'Y', new WikitextContent( 'Y' ) ),
+                       'Z' => $this->newSavedSlot( 'Z', new WikitextContent( 'Z' ) ),
+               ];
+               $slots = MutableRevisionSlots::newFromParentRevisionSlots( $parentSlots );
+               $update = RevisionSlotsUpdate::newFromContent( [
+                       'A' => new WikitextContent( 'A' ),
+                       'Y' => new WikitextContent( 'yyy' ),
+               ] );
+
+               $update->removeSlot( 'Z' );
+
+               $update->apply( $slots );
+               $this->assertSame( [ 'X', 'Y', 'A' ], $slots->getSlotRoles() );
+               $this->assertSame( $update->getModifiedSlot( 'A' ), $slots->getSlot( 'A' ) );
+               $this->assertSame( $update->getModifiedSlot( 'Y' ), $slots->getSlot( 'Y' ) );
+       }
+
 }
index bdff4cd..110f032 100644 (file)
@@ -3,6 +3,7 @@
 namespace MediaWiki\Tests\Storage;
 
 use CommentStoreComment;
+use Content;
 use Exception;
 use HashBagOStuff;
 use InvalidArgumentException;
@@ -36,6 +37,16 @@ use WikitextContent;
  */
 abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
 
+       /**
+        * @var Title
+        */
+       private $testPageTitle;
+
+       /**
+        * @var WikiPage
+        */
+       private $testPage;
+
        /**
         * @return int
         */
@@ -79,6 +90,50 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                $this->overrideMwServices();
        }
 
+       protected function addCoreDBData() {
+               // Blank out. This would fail with a modified schema, and we don't need it.
+       }
+
+       /**
+        * @return Title
+        */
+       protected function getTestPageTitle() {
+               if ( $this->testPageTitle ) {
+                       return $this->testPageTitle;
+               }
+
+               $this->testPageTitle = Title::newFromText( 'UTPage-' . __CLASS__ );
+               return $this->testPageTitle;
+       }
+       /**
+        * @return WikiPage
+        */
+       protected function getTestPage() {
+               if ( $this->testPage ) {
+                       return $this->testPage;
+               }
+
+               $title = $this->getTestPageTitle();
+               $this->testPage = WikiPage::factory( $title );
+
+               if ( !$this->testPage->exists() ) {
+                       // Make sure we don't write to the live db.
+                       $this->ensureMockDatabaseConnection( wfGetDB( DB_MASTER ) );
+
+                       $user = static::getTestSysop()->getUser();
+
+                       $this->testPage->doEditContent(
+                               new WikitextContent( 'UTContent-' . __CLASS__ ),
+                               'UTPageSummary-' . __CLASS__,
+                               EDIT_NEW | EDIT_SUPPRESS_RC,
+                               false,
+                               $user
+                       );
+               }
+
+               return $this->testPage;
+       }
+
        /**
         * @return LoadBalancer|PHPUnit_Framework_MockObject_MockObject
         */
@@ -175,6 +230,9 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                        $blobStore,
                        new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ),
                        MediaWikiServices::getInstance()->getCommentStore(),
+                       MediaWikiServices::getInstance()->getContentModelStore(),
+                       MediaWikiServices::getInstance()->getSlotRoleStore(),
+                       $this->getMcrMigrationStage(),
                        MediaWikiServices::getInstance()->getActorMigration(),
                        $wikiId
                );
@@ -194,17 +252,29 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
        }
 
        private function assertRevisionRecordsEqual( RevisionRecord $r1, RevisionRecord $r2 ) {
+               $this->assertEquals(
+                       $r1->getPageAsLinkTarget()->getNamespace(),
+                       $r2->getPageAsLinkTarget()->getNamespace()
+               );
+
+               $this->assertEquals(
+                       $r1->getPageAsLinkTarget()->getText(),
+                       $r2->getPageAsLinkTarget()->getText()
+               );
+
+               if ( $r1->getParentId() ) {
+                       $this->assertEquals( $r1->getParentId(), $r2->getParentId() );
+               }
+
                $this->assertEquals( $r1->getUser()->getName(), $r2->getUser()->getName() );
                $this->assertEquals( $r1->getUser()->getId(), $r2->getUser()->getId() );
                $this->assertEquals( $r1->getComment(), $r2->getComment() );
-               $this->assertEquals( $r1->getPageAsLinkTarget(), $r2->getPageAsLinkTarget() );
                $this->assertEquals( $r1->getTimestamp(), $r2->getTimestamp() );
                $this->assertEquals( $r1->getVisibility(), $r2->getVisibility() );
                $this->assertEquals( $r1->getSha1(), $r2->getSha1() );
-               $this->assertEquals( $r1->getParentId(), $r2->getParentId() );
                $this->assertEquals( $r1->getSize(), $r2->getSize() );
                $this->assertEquals( $r1->getPageId(), $r2->getPageId() );
-               $this->assertEquals( $r1->getSlotRoles(), $r2->getSlotRoles() );
+               $this->assertArrayEqualsIgnoringIntKeyOrder( $r1->getSlotRoles(), $r2->getSlotRoles() );
                $this->assertEquals( $r1->getWikiId(), $r2->getWikiId() );
                $this->assertEquals( $r1->isMinor(), $r2->isMinor() );
                foreach ( $r1->getSlotRoles() as $role ) {
@@ -234,6 +304,10 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
        }
 
        private function assertRevisionCompleteness( RevisionRecord $r ) {
+               $this->assertTrue( $r->hasSlot( 'main' ) );
+               $this->assertInstanceOf( SlotRecord::class, $r->getSlot( 'main' ) );
+               $this->assertInstanceOf( Content::class, $r->getContent( 'main' ) );
+
                foreach ( $r->getSlotRoles() as $role ) {
                        $this->assertSlotCompleteness( $r, $r->getSlot( $role ) );
                }
@@ -242,6 +316,8 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
        private function assertSlotCompleteness( RevisionRecord $r, SlotRecord $slot ) {
                $this->assertTrue( $slot->hasAddress() );
                $this->assertSame( $r->getId(), $slot->getRevision() );
+
+               $this->assertInstanceOf( Content::class, $slot->getContent() );
        }
 
        /**
@@ -249,21 +325,20 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         *
         * @return RevisionRecord
         */
-       private function getRevisionRecordFromDetailsArray( $title, $details = [] ) {
+       private function getRevisionRecordFromDetailsArray( $details = [] ) {
                // Convert some values that can't be provided by dataProviders
-               $page = WikiPage::factory( $title );
                if ( isset( $details['user'] ) && $details['user'] === true ) {
                        $details['user'] = $this->getTestUser()->getUser();
                }
                if ( isset( $details['page'] ) && $details['page'] === true ) {
-                       $details['page'] = $page->getId();
+                       $details['page'] = $this->getTestPage()->getId();
                }
                if ( isset( $details['parent'] ) && $details['parent'] === true ) {
-                       $details['parent'] = $page->getLatest();
+                       $details['parent'] = $this->getTestPage()->getLatest();
                }
 
                // Create the RevisionRecord with any available data
-               $rev = new MutableRevisionRecord( $title );
+               $rev = new MutableRevisionRecord( $this->getTestPageTitle() );
                isset( $details['slot'] ) ? $rev->setSlot( $details['slot'] ) : null;
                isset( $details['parent'] ) ? $rev->setParentId( $details['parent'] ) : null;
                isset( $details['page'] ) ? $rev->setPageId( $details['page'] ) : null;
@@ -276,22 +351,26 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                isset( $details['visibility'] ) ? $rev->setVisibility( $details['visibility'] ) : null;
                isset( $details['id'] ) ? $rev->setId( $details['id'] ) : null;
 
+               if ( isset( $details['content'] ) ) {
+                       foreach ( $details['content'] as $role => $content ) {
+                               $rev->setContent( $role, $content );
+                       }
+               }
+
                return $rev;
        }
 
        public function provideInsertRevisionOn_successes() {
                yield 'Bare minimum revision insertion' => [
-                       Title::newFromText( 'UTPage' ),
                        [
                                'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ),
-                               'parent' => true,
+                               'page' => true,
                                'comment' => $this->getRandomCommentStoreComment(),
                                'timestamp' => '20171117010101',
                                'user' => true,
                        ],
                ];
                yield 'Detailed revision insertion' => [
-                       Title::newFromText( 'UTPage' ),
                        [
                                'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ),
                                'parent' => true,
@@ -305,33 +384,64 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                ];
        }
 
-       private function getRandomCommentStoreComment() {
+       protected function getRandomCommentStoreComment() {
                return CommentStoreComment::newUnsavedComment( __METHOD__ . '.' . rand( 0, 1000 ) );
        }
 
        /**
         * @dataProvider provideInsertRevisionOn_successes
         * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn
+        * @covers \MediaWiki\Storage\RevisionStore::insertSlotRowOn
+        * @covers \MediaWiki\Storage\RevisionStore::insertContentRowOn
         */
        public function testInsertRevisionOn_successes(
-               Title $title,
                array $revDetails = []
        ) {
-               $rev = $this->getRevisionRecordFromDetailsArray( $title, $revDetails );
+               $title = $this->getTestPageTitle();
+               $rev = $this->getRevisionRecordFromDetailsArray( $revDetails );
 
                $this->overrideMwServices();
                $store = MediaWikiServices::getInstance()->getRevisionStore();
                $return = $store->insertRevisionOn( $rev, wfGetDB( DB_MASTER ) );
 
+               // is the new revision correct?
+               $this->assertRevisionCompleteness( $return );
                $this->assertLinkTargetsEqual( $title, $return->getPageAsLinkTarget() );
                $this->assertRevisionRecordsEqual( $rev, $return );
-               $this->assertRevisionCompleteness( $return );
+
+               // can we load it from the store?
+               $loaded = $store->getRevisionById( $return->getId() );
+               $this->assertRevisionCompleteness( $loaded );
+               $this->assertRevisionRecordsEqual( $return, $loaded );
+
+               // can we find it directly in the database?
                $this->assertRevisionExistsInDatabase( $return );
        }
 
        protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) {
+               $row = $this->revisionToRow( new Revision( $rev ), [] );
+
+               // unset nulled fields
+               unset( $row->rev_content_model );
+               unset( $row->rev_content_format );
+
+               // unset fake fields
+               unset( $row->rev_comment_text );
+               unset( $row->rev_comment_data );
+               unset( $row->rev_comment_cid );
+               unset( $row->rev_comment_id );
+
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $queryInfo = $store->getQueryInfo( [ 'user' ] );
+
+               $row = get_object_vars( $row );
                $this->assertSelect(
-                       'revision', [ 'count(*)' ], [ 'rev_id' => $rev->getId() ], [ [ '1' ] ]
+                       $queryInfo['tables'],
+                       array_keys( $row ),
+                       [ 'rev_id' => $rev->getId() ],
+                       [ array_values( $row ) ],
+                       [],
+                       $queryInfo['joins']
                );
        }
 
@@ -348,7 +458,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn
         */
        public function testInsertRevisionOn_blobAddressExists() {
-               $title = Title::newFromText( 'UTPage' );
+               $title = $this->getTestPageTitle();
                $revDetails = [
                        'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ),
                        'parent' => true,
@@ -361,14 +471,14 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                $store = MediaWikiServices::getInstance()->getRevisionStore();
 
                // Insert the first revision
-               $revOne = $this->getRevisionRecordFromDetailsArray( $title, $revDetails );
+               $revOne = $this->getRevisionRecordFromDetailsArray( $revDetails );
                $firstReturn = $store->insertRevisionOn( $revOne, wfGetDB( DB_MASTER ) );
                $this->assertLinkTargetsEqual( $title, $firstReturn->getPageAsLinkTarget() );
                $this->assertRevisionRecordsEqual( $revOne, $firstReturn );
 
                // Insert a second revision inheriting the same blob address
                $revDetails['slot'] = SlotRecord::newInherited( $firstReturn->getSlot( 'main' ) );
-               $revTwo = $this->getRevisionRecordFromDetailsArray( $title, $revDetails );
+               $revTwo = $this->getRevisionRecordFromDetailsArray( $revDetails );
                $secondReturn = $store->insertRevisionOn( $revTwo, wfGetDB( DB_MASTER ) );
                $this->assertLinkTargetsEqual( $title, $secondReturn->getPageAsLinkTarget() );
                $this->assertRevisionRecordsEqual( $revTwo, $secondReturn );
@@ -388,26 +498,23 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
 
        public function provideInsertRevisionOn_failures() {
                yield 'no slot' => [
-                       Title::newFromText( 'UTPage' ),
                        [
                                'comment' => $this->getRandomCommentStoreComment(),
                                'timestamp' => '20171117010101',
                                'user' => true,
                        ],
-                       new InvalidArgumentException( 'At least one slot needs to be defined!' )
+                       new InvalidArgumentException( 'main slot must be provided' )
                ];
-               yield 'slot that is not main slot' => [
-                       Title::newFromText( 'UTPage' ),
+               yield 'no main slot' => [
                        [
-                               'slot' => SlotRecord::newUnsaved( 'lalala', new WikitextContent( 'Chicken' ) ),
+                               'slot' => SlotRecord::newUnsaved( 'aux', new WikitextContent( 'Turkey' ) ),
                                'comment' => $this->getRandomCommentStoreComment(),
                                'timestamp' => '20171117010101',
                                'user' => true,
                        ],
-                       new InvalidArgumentException( 'Only the main slot is supported for now!' )
+                       new InvalidArgumentException( 'main slot must be provided' )
                ];
                yield 'no timestamp' => [
-                       Title::newFromText( 'UTPage' ),
                        [
                                'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ),
                                'comment' => $this->getRandomCommentStoreComment(),
@@ -416,7 +523,6 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                        new IncompleteRevisionException( 'timestamp field must not be NULL!' )
                ];
                yield 'no comment' => [
-                       Title::newFromText( 'UTPage' ),
                        [
                                'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ),
                                'timestamp' => '20171117010101',
@@ -425,7 +531,6 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                        new IncompleteRevisionException( 'comment must not be NULL!' )
                ];
                yield 'no user' => [
-                       Title::newFromText( 'UTPage' ),
                        [
                                'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ),
                                'comment' => $this->getRandomCommentStoreComment(),
@@ -440,11 +545,10 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn
         */
        public function testInsertRevisionOn_failures(
-               Title $title,
                array $revDetails = [],
                Exception $exception
        ) {
-               $rev = $this->getRevisionRecordFromDetailsArray( $title, $revDetails );
+               $rev = $this->getRevisionRecordFromDetailsArray( $revDetails );
 
                $store = MediaWikiServices::getInstance()->getRevisionStore();
 
@@ -459,11 +563,13 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
        public function provideNewNullRevision() {
                yield [
                        Title::newFromText( 'UTPage_notAutoCreated' ),
+                       [ 'content' => [ 'main' => new WikitextContent( 'Flubber1' ) ] ],
                        CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment1' ),
                        true,
                ];
                yield [
                        Title::newFromText( 'UTPage_notAutoCreated' ),
+                       [ 'content' => [ 'main' => new WikitextContent( 'Flubber2' ) ] ],
                        CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment2', [ 'a' => 1 ] ),
                        false,
                ];
@@ -472,24 +578,30 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
        /**
         * @dataProvider provideNewNullRevision
         * @covers \MediaWiki\Storage\RevisionStore::newNullRevision
+        * @covers \MediaWiki\Storage\RevisionStore::findSlotContentId
         */
-       public function testNewNullRevision( Title $title, $comment, $minor ) {
+       public function testNewNullRevision( Title $title, $revDetails, $comment, $minor = false ) {
                $this->overrideMwServices();
 
+               $user = TestUserRegistry::getMutableTestUser( __METHOD__ )->getUser();
                $page = WikiPage::factory( $title );
-               $status = $page->doEditContent(
-                       new WikitextContent( __METHOD__ ),
-                       __METHOD__,
-                       0,
-                       false
-               );
-               /** @var Revision $rev */
-               $rev = $status->value['revision'];
 
+               if ( !$page->exists() ) {
+                       $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__, EDIT_NEW );
+               }
+
+               $revDetails['page'] = $page->getId();
+               $revDetails['timestamp'] = wfTimestampNow();
+               $revDetails['comment'] = CommentStoreComment::newUnsavedComment( 'Base' );
+               $revDetails['user'] = $user;
+
+               $baseRev = $this->getRevisionRecordFromDetailsArray( $revDetails );
                $store = MediaWikiServices::getInstance()->getRevisionStore();
-               $user = TestUserRegistry::getMutableTestUser( __METHOD__ )->getUser();
 
-               $parent = $store->getRevisionById( $rev->getId() );
+               $dbw = wfGetDB( DB_MASTER );
+               $baseRev = $store->insertRevisionOn( $baseRev, $dbw );
+               $page->updateRevisionOn( $dbw, new Revision( $baseRev ), $page->getLatest() );
+
                $record = $store->newNullRevision(
                        wfGetDB( DB_MASTER ),
                        $title,
@@ -503,14 +615,21 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                $this->assertEquals( $comment, $record->getComment() );
                $this->assertEquals( $minor, $record->isMinor() );
                $this->assertEquals( $user->getName(), $record->getUser()->getName() );
-               $this->assertEquals( $parent->getId(), $record->getParentId() );
+               $this->assertEquals( $baseRev->getId(), $record->getParentId() );
+
+               $this->assertArrayEqualsIgnoringIntKeyOrder(
+                       $baseRev->getSlotRoles(),
+                       $record->getSlotRoles()
+               );
 
-               $parentSlot = $parent->getSlot( 'main' );
-               $slot = $record->getSlot( 'main' );
+               foreach ( $baseRev->getSlotRoles() as $role ) {
+                       $parentSlot = $baseRev->getSlot( $role );
+                       $slot = $record->getSlot( $role );
 
-               $this->assertTrue( $slot->isInherited(), 'isInherited' );
-               $this->assertSame( $parentSlot->getOrigin(), $slot->getOrigin(), 'getOrigin' );
-               $this->assertSameSlotContent( $parentSlot, $slot );
+                       $this->assertTrue( $slot->isInherited(), 'isInherited' );
+                       $this->assertSame( $parentSlot->getOrigin(), $slot->getOrigin(), 'getOrigin' );
+                       $this->assertSameSlotContent( $parentSlot, $slot );
+               }
        }
 
        /**
@@ -532,7 +651,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         * @covers \MediaWiki\Storage\RevisionStore::getRcIdIfUnpatrolled
         */
        public function testGetRcIdIfUnpatrolled_returnsRecentChangesId() {
-               $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
+               $page = $this->getTestPage();
                $status = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
                /** @var Revision $rev */
                $rev = $status->value['revision'];
@@ -543,7 +662,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
 
                $this->assertGreaterThan( 0, $result );
                $this->assertSame(
-                       $page->getRevision()->getRecentChange()->getAttribute( 'rc_id' ),
+                       $store->getRecentChange( $revisionRecord )->getAttribute( 'rc_id' ),
                        $result
                );
        }
@@ -554,7 +673,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
        public function testGetRcIdIfUnpatrolled_returnsZeroIfPatrolled() {
                // This assumes that sysops are auto patrolled
                $sysop = $this->getTestSysop()->getUser();
-               $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
+               $page = $this->getTestPage();
                $status = $page->doEditContent(
                        new WikitextContent( __METHOD__ ),
                        __METHOD__,
@@ -576,7 +695,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         * @covers \MediaWiki\Storage\RevisionStore::getRecentChange
         */
        public function testGetRecentChange() {
-               $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
+               $page = $this->getTestPage();
                $content = new WikitextContent( __METHOD__ );
                $status = $page->doEditContent( $content, __METHOD__ );
                /** @var Revision $rev */
@@ -594,7 +713,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         * @covers \MediaWiki\Storage\RevisionStore::getRevisionById
         */
        public function testGetRevisionById() {
-               $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
+               $page = $this->getTestPage();
                $content = new WikitextContent( __METHOD__ );
                $status = $page->doEditContent( $content, __METHOD__ );
                /** @var Revision $rev */
@@ -612,7 +731,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         * @covers \MediaWiki\Storage\RevisionStore::getRevisionByTitle
         */
        public function testGetRevisionByTitle() {
-               $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
+               $page = $this->getTestPage();
                $content = new WikitextContent( __METHOD__ );
                $status = $page->doEditContent( $content, __METHOD__ );
                /** @var Revision $rev */
@@ -630,7 +749,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         * @covers \MediaWiki\Storage\RevisionStore::getRevisionByPageId
         */
        public function testGetRevisionByPageId() {
-               $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
+               $page = $this->getTestPage();
                $content = new WikitextContent( __METHOD__ );
                $status = $page->doEditContent( $content, __METHOD__ );
                /** @var Revision $rev */
@@ -651,8 +770,8 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                // Make sure there is 1 second between the last revision and the rev we create...
                // Otherwise we might not get the correct revision and the test may fail...
                // :(
+               $page = $this->getTestPage();
                sleep( 1 );
-               $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
                $content = new WikitextContent( __METHOD__ );
                $status = $page->doEditContent( $content, __METHOD__ );
                /** @var Revision $rev */
@@ -669,13 +788,13 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                $this->assertSame( __METHOD__, $revRecord->getComment()->text );
        }
 
-       protected function revisionToRow( Revision $rev ) {
+       protected function revisionToRow( Revision $rev, $options = [ 'page', 'user', 'comment' ] ) {
+               // XXX: the WikiPage object loads another RevisionRecord from the database. Not great.
                $page = WikiPage::factory( $rev->getTitle() );
 
-               return (object)[
+               $fields = [
                        'rev_id' => (string)$rev->getId(),
                        'rev_page' => (string)$rev->getPage(),
-                       'rev_text_id' => (string)$rev->getTextId(),
                        'rev_timestamp' => $this->db->timestamp( $rev->getTimestamp() ),
                        'rev_user_text' => (string)$rev->getUserText(),
                        'rev_user' => (string)$rev->getUser(),
@@ -684,19 +803,40 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                        'rev_len' => (string)$rev->getSize(),
                        'rev_parent_id' => (string)$rev->getParentId(),
                        'rev_sha1' => (string)$rev->getSha1(),
-                       'rev_comment_text' => $rev->getComment(),
-                       'rev_comment_data' => null,
-                       'rev_comment_cid' => null,
-                       'rev_content_format' => $rev->getContentFormat(),
-                       'rev_content_model' => $rev->getContentModel(),
-                       'page_namespace' => (string)$page->getTitle()->getNamespace(),
-                       'page_title' => $page->getTitle()->getDBkey(),
-                       'page_id' => (string)$page->getId(),
-                       'page_latest' => (string)$page->getLatest(),
-                       'page_is_redirect' => $page->isRedirect() ? '1' : '0',
-                       'page_len' => (string)$page->getContent()->getSize(),
-                       'user_name' => (string)$rev->getUserText(),
                ];
+
+               if ( in_array( 'page', $options ) ) {
+                       $fields += [
+                               'page_namespace' => (string)$page->getTitle()->getNamespace(),
+                               'page_title' => $page->getTitle()->getDBkey(),
+                               'page_id' => (string)$page->getId(),
+                               'page_latest' => (string)$page->getLatest(),
+                               'page_is_redirect' => $page->isRedirect() ? '1' : '0',
+                               'page_len' => (string)$page->getContent()->getSize(),
+                       ];
+               }
+
+               if ( in_array( 'user', $options ) ) {
+                       $fields += [
+                               'user_name' => (string)$rev->getUserText(),
+                       ];
+               }
+
+               if ( in_array( 'comment', $options ) ) {
+                       $fields += [
+                               'rev_comment_text' => $rev->getComment(),
+                               'rev_comment_data' => null,
+                               'rev_comment_cid' => null,
+                       ];
+               }
+
+               if ( $rev->getId() ) {
+                       $fields += [
+                               'rev_id' => (string)$rev->getId(),
+                       ];
+               }
+
+               return (object)$fields;
        }
 
        private function assertRevisionRecordMatchesRevision(
@@ -725,14 +865,33 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                $this->assertSame( $rev->getContentFormat(), $record->getContent( 'main' )->getDefaultFormat() );
                $this->assertSame( $rev->getContentModel(), $record->getContent( 'main' )->getModel() );
                $this->assertLinkTargetsEqual( $rev->getTitle(), $record->getPageAsLinkTarget() );
+
+               $revRec = $rev->getRevisionRecord();
+               $revMain = $revRec->getSlot( 'main' );
+               $recMain = $record->getSlot( 'main' );
+
+               $this->assertSame( $revMain->hasOrigin(), $recMain->hasOrigin(), 'hasOrigin' );
+               $this->assertSame( $revMain->hasAddress(), $recMain->hasAddress(), 'hasAddress' );
+               $this->assertSame( $revMain->hasContentId(), $recMain->hasContentId(), 'hasContentId' );
+
+               if ( $revMain->hasOrigin() ) {
+                       $this->assertSame( $revMain->getOrigin(), $recMain->getOrigin(), 'getOrigin' );
+               }
+
+               if ( $revMain->hasAddress() ) {
+                       $this->assertSame( $revMain->getAddress(), $recMain->getAddress(), 'getAddress' );
+               }
+
+               if ( $revMain->hasContentId() ) {
+                       $this->assertSame( $revMain->getContentId(), $recMain->getContentId(), 'getContentId' );
+               }
        }
 
        /**
         * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
-        * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
         */
        public function testNewRevisionFromRow_anonEdit() {
-               $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
+               $page = $this->getTestPage();
                $text = __METHOD__ . 'a-ä';
                /** @var Revision $rev */
                $rev = $page->doEditContent(
@@ -752,12 +911,11 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
 
        /**
         * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
-        * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
         */
        public function testNewRevisionFromRow_anonEdit_legacyEncoding() {
                $this->setMwGlobals( 'wgLegacyEncoding', 'windows-1252' );
                $this->overrideMwServices();
-               $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
+               $page = $this->getTestPage();
                $text = __METHOD__ . 'a-ä';
                /** @var Revision $rev */
                $rev = $page->doEditContent(
@@ -777,10 +935,9 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
 
        /**
         * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
-        * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
         */
        public function testNewRevisionFromRow_userEdit() {
-               $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
+               $page = $this->getTestPage();
                $text = __METHOD__ . 'b-ä';
                /** @var Revision $rev */
                $rev = $page->doEditContent(
@@ -861,6 +1018,58 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                $this->assertSame( $text, $record->getContent( 'main' )->serialize() );
        }
 
+       /**
+        * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn
+        */
+       public function testInsertRevisionOn_archive() {
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+               $title = Title::newFromText( __METHOD__ );
+
+               $page = WikiPage::factory( $title );
+               /** @var Revision $origRev */
+               $page->doEditContent( new WikitextContent( "First" ), __METHOD__ . '-first' );
+               $origRev = $page->doEditContent( new WikitextContent( "Foo" ), __METHOD__ )
+                       ->value['revision'];
+               $orig = $origRev->getRevisionRecord();
+               $page->doDeleteArticle( __METHOD__ );
+
+               $db = wfGetDB( DB_MASTER );
+               $arQuery = $store->getArchiveQueryInfo();
+               $row = $db->selectRow(
+                       $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ],
+                       __METHOD__, [], $arQuery['joins']
+               );
+
+               $record = $store->newRevisionFromArchiveRow( $row );
+
+               $restored = $store->insertRevisionOn( $record, $db );
+               $this->assertSame( $orig->getPageId(), $restored->getPageId() );
+               $this->assertSame( $orig->getId(), $restored->getId() );
+               $this->assertSame( $orig->getComment()->text, $restored->getComment()->text );
+
+               $origMain = $orig->getSlot( 'main' );
+               $restoredMain = $restored->getSlot( 'main' );
+               $this->assertSame(
+                       $origMain->getOrigin(),
+                       $restoredMain->getOrigin()
+               );
+
+               if ( $origMain->hasContentId() ) {
+                       $this->assertSame(
+                               $origMain->getContentId(),
+                               $restoredMain->getContentId()
+                       );
+               }
+
+               // NOTE: we didn't restore the page row, so we can't use RevisionStore::getRevisionById
+               $this->assertSelect(
+                       'revision',
+                       [ 'rev_id' ],
+                       [ 'rev_id' => $orig->getId() ],
+                       [ [ $orig->getId() ] ]
+               );
+       }
+
        /**
         * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromId
         */
@@ -1031,7 +1240,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         * @covers \MediaWiki\Storage\RevisionStore::getTimestampFromId
         */
        public function testGetTimestampFromId_found() {
-               $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
+               $page = $this->getTestPage();
                /** @var Revision $rev */
                $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
                        ->value['revision'];
@@ -1049,7 +1258,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         * @covers \MediaWiki\Storage\RevisionStore::getTimestampFromId
         */
        public function testGetTimestampFromId_notFound() {
-               $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
+               $page = $this->getTestPage();
                /** @var Revision $rev */
                $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
                        ->value['revision'];
@@ -1114,7 +1323,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         */
        public function testUserWasLastToEdit_false() {
                $sysop = $this->getTestSysop()->getUser();
-               $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
+               $page = $this->getTestPage();
                $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
 
                $store = MediaWikiServices::getInstance()->getRevisionStore();
@@ -1133,7 +1342,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
        public function testUserWasLastToEdit_true() {
                $startTime = wfTimestampNow();
                $sysop = $this->getTestSysop()->getUser();
-               $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
+               $page = $this->getTestPage();
                $page->doEditContent(
                        new WikitextContent( __METHOD__ ),
                        __METHOD__,
@@ -1156,7 +1365,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
         * @covers \MediaWiki\Storage\RevisionStore::getKnownCurrentRevision
         */
        public function testGetKnownCurrentRevision() {
-               $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
+               $page = $this->getTestPage();
                /** @var Revision $rev */
                $rev = $page->doEditContent(
                        new WikitextContent( __METHOD__ . 'b' ),
@@ -1176,24 +1385,6 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
        }
 
        public function provideNewMutableRevisionFromArray() {
-               yield 'Basic array, with page & id' => [
-                       [
-                               'id' => 2,
-                               'page' => 1,
-                               'text_id' => 2,
-                               'timestamp' => '20171017114835',
-                               'user_text' => '111.0.1.2',
-                               'user' => 0,
-                               'minor_edit' => false,
-                               'deleted' => 0,
-                               'len' => 46,
-                               'parent_id' => 1,
-                               'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
-                               'comment' => 'Goat Comment!',
-                               'content_format' => 'text/x-wiki',
-                               'content_model' => 'wikitext',
-                       ]
-               ];
                yield 'Basic array, content object' => [
                        [
                                'id' => 2,
@@ -1246,7 +1437,6 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                yield 'Basic array, with title' => [
                        [
                                'title' => Title::newFromText( 'SomeText' ),
-                               'text_id' => 2,
                                'timestamp' => '20171017114835',
                                'user_text' => '111.0.1.2',
                                'user' => 0,
@@ -1256,15 +1446,13 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                                'parent_id' => 1,
                                'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
                                'comment' => 'Goat Comment!',
-                               'content_format' => 'text/x-wiki',
-                               'content_model' => 'wikitext',
+                               'content' => new WikitextContent( 'Some Content' ),
                        ]
                ];
                yield 'Basic array, no user field' => [
                        [
                                'id' => 2,
                                'page' => 1,
-                               'text_id' => 2,
                                'timestamp' => '20171017114835',
                                'user_text' => '111.0.1.3',
                                'minor_edit' => false,
@@ -1273,8 +1461,7 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                                'parent_id' => 1,
                                'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
                                'comment' => 'Goat Comment!',
-                               'content_format' => 'text/x-wiki',
-                               'content_model' => 'wikitext',
+                               'content' => new WikitextContent( 'Some Content' ),
                        ]
                ];
        }
@@ -1286,6 +1473,16 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
        public function testNewMutableRevisionFromArray( array $array ) {
                $store = MediaWikiServices::getInstance()->getRevisionStore();
 
+               // HACK: if $array['page'] is given, make sure that the page exists
+               if ( isset( $array['page'] ) ) {
+                       $t = Title::newFromID( $array['page'] );
+                       if ( !$t || !$t->exists() ) {
+                               $t = Title::makeTitle( NS_MAIN, __METHOD__ );
+                               $info = $this->insertPage( $t );
+                               $array['page'] = $info['id'];
+                       }
+               }
+
                $result = $store->newMutableRevisionFromArray( $array );
 
                if ( isset( $array['id'] ) ) {
@@ -1306,12 +1503,14 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                $this->assertSame( $array['sha1'], $result->getSha1() );
                $this->assertSame( $array['comment'], $result->getComment()->text );
                if ( isset( $array['content'] ) ) {
-                       $this->assertTrue(
-                               $result->getSlot( 'main' )->getContent()->equals( $array['content'] )
-                       );
+                       foreach ( $array['content'] as $role => $content ) {
+                               $this->assertTrue(
+                                       $result->getContent( $role )->equals( $content )
+                               );
+                       }
                } elseif ( isset( $array['text'] ) ) {
                        $this->assertSame( $array['text'], $result->getSlot( 'main' )->getContent()->serialize() );
-               } else {
+               } elseif ( isset( $array['content_format'] ) ) {
                        $this->assertSame(
                                $array['content_format'],
                                $result->getSlot( 'main' )->getContent()->getDefaultFormat()
@@ -1458,6 +1657,33 @@ abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
                );
        }
 
+       abstract public function provideGetSlotsQueryInfo();
+
+       /**
+        * @dataProvider provideGetSlotsQueryInfo
+        * @covers \MediaWiki\Storage\RevisionStore::getSlotsQueryInfo
+        */
+       public function testGetSlotsQueryInfo( $options, $expected ) {
+               $store = MediaWikiServices::getInstance()->getRevisionStore();
+
+               $archiveQueryInfo = $store->getSlotsQueryInfo( $options );
+
+               $this->assertArrayEqualsIgnoringIntKeyOrder(
+                       $expected['tables'],
+                       $archiveQueryInfo['tables']
+               );
+
+               $this->assertArrayEqualsIgnoringIntKeyOrder(
+                       $expected['fields'],
+                       $archiveQueryInfo['fields']
+               );
+
+               $this->assertArrayEqualsIgnoringIntKeyOrder(
+                       $expected['joins'],
+                       $archiveQueryInfo['joins']
+               );
+       }
+
        /**
         * Assert that the two arrays passed are equal, ignoring the order of the values that integer
         * keys.
index 0295e90..1d6a9a0 100644 (file)
@@ -168,7 +168,9 @@ class RevisionStoreRecordTest extends MediaWikiTestCase {
                $this->assertSame( $user, $rec->getUser( RevisionRecord::RAW ), 'getUser' );
                $this->assertSame( $comment, $rec->getComment(), 'getComment' );
 
+               $this->assertSame( $slots, $rec->getSlots(), 'getSlots' );
                $this->assertSame( $slots->getSlotRoles(), $rec->getSlotRoles(), 'getSlotRoles' );
+               $this->assertSame( $slots->getSlots(), $rec->getSlots()->getSlots(), 'getSlots' );
                $this->assertSame( $wikiId, $rec->getWikiId(), 'getWikiId' );
 
                $this->assertSame( (int)$row->rev_id, $rec->getId(), 'getId' );
index 61d0512..727697c 100644 (file)
@@ -2,17 +2,21 @@
 
 namespace MediaWiki\Tests\Storage;
 
+use CommentStore;
 use HashBagOStuff;
+use InvalidArgumentException;
 use Language;
 use MediaWiki\MediaWikiServices;
 use MediaWiki\Storage\RevisionAccessException;
 use MediaWiki\Storage\RevisionStore;
 use MediaWiki\Storage\SqlBlobStore;
 use MediaWikiTestCase;
+use MWException;
 use Title;
 use WANObjectCache;
 use Wikimedia\Rdbms\Database;
 use Wikimedia\Rdbms\LoadBalancer;
+use Wikimedia\TestingAccessWrapper;
 
 class RevisionStoreTest extends MediaWikiTestCase {
 
@@ -28,11 +32,18 @@ class RevisionStoreTest extends MediaWikiTestCase {
                $blobStore = null,
                $WANObjectCache = null
        ) {
+               global $wgMultiContentRevisionSchemaMigrationStage;
+               // the migration stage should be irrelevant, since all the tests that interact with
+               // the database are in RevisionStoreDbTest, not here.
+
                return new RevisionStore(
                        $loadBalancer ?: $this->getMockLoadBalancer(),
                        $blobStore ?: $this->getMockSqlBlobStore(),
                        $WANObjectCache ?: $this->getHashWANObjectCache(),
                        MediaWikiServices::getInstance()->getCommentStore(),
+                       MediaWikiServices::getInstance()->getContentModelStore(),
+                       MediaWikiServices::getInstance()->getSlotRoleStore(),
+                       $wgMultiContentRevisionSchemaMigrationStage,
                        MediaWikiServices::getInstance()->getActorMigration()
                );
        }
@@ -61,190 +72,53 @@ class RevisionStoreTest extends MediaWikiTestCase {
                        ->disableOriginalConstructor()->getMock();
        }
 
-       private function getHashWANObjectCache() {
-               return new WANObjectCache( [ 'cache' => new \HashBagOStuff() ] );
-       }
-
        /**
-        * @covers \MediaWiki\Storage\RevisionStore::getContentHandlerUseDB
-        * @covers \MediaWiki\Storage\RevisionStore::setContentHandlerUseDB
+        * @return \PHPUnit_Framework_MockObject_MockObject|CommentStore
         */
-       public function testGetSetContentHandlerDb() {
-               $store = $this->getRevisionStore();
-               $this->assertTrue( $store->getContentHandlerUseDB() );
-               $store->setContentHandlerUseDB( false );
-               $this->assertFalse( $store->getContentHandlerUseDB() );
-               $store->setContentHandlerUseDB( true );
-               $this->assertTrue( $store->getContentHandlerUseDB() );
-       }
-
-       private function getDefaultQueryFields() {
-               return [
-                       'rev_id',
-                       'rev_page',
-                       'rev_text_id',
-                       'rev_timestamp',
-                       'rev_minor_edit',
-                       'rev_deleted',
-                       'rev_len',
-                       'rev_parent_id',
-                       'rev_sha1',
-               ];
-       }
-
-       private function getCommentQueryFields() {
-               return [
-                       'rev_comment_text' => 'rev_comment',
-                       'rev_comment_data' => 'NULL',
-                       'rev_comment_cid' => 'NULL',
-               ];
+       private function getMockCommentStore() {
+               return $this->getMockBuilder( CommentStore::class )
+                       ->disableOriginalConstructor()->getMock();
        }
 
-       private function getActorQueryFields() {
-               return [
-                       'rev_user' => 'rev_user',
-                       'rev_user_text' => 'rev_user_text',
-                       'rev_actor' => 'NULL',
-               ];
+       private function getHashWANObjectCache() {
+               return new WANObjectCache( [ 'cache' => new \HashBagOStuff() ] );
        }
 
-       private function getContentHandlerQueryFields() {
+       public function provideSetContentHandlerUseDB() {
                return [
-                       'rev_content_format',
-                       'rev_content_model',
-               ];
-       }
-
-       public function provideGetQueryInfo() {
-               yield [
-                       true,
-                       [],
-                       [
-                               'tables' => [ 'revision' ],
-                               'fields' => array_merge(
-                                       $this->getDefaultQueryFields(),
-                                       $this->getCommentQueryFields(),
-                                       $this->getActorQueryFields(),
-                                       $this->getContentHandlerQueryFields()
-                               ),
-                               'joins' => [],
-                       ]
-               ];
-               yield [
-                       false,
-                       [],
-                       [
-                               'tables' => [ 'revision' ],
-                               'fields' => array_merge(
-                                       $this->getDefaultQueryFields(),
-                                       $this->getCommentQueryFields(),
-                                       $this->getActorQueryFields()
-                               ),
-                               'joins' => [],
-                       ]
-               ];
-               yield [
-                       false,
-                       [ 'page' ],
-                       [
-                               'tables' => [ 'revision', 'page' ],
-                               'fields' => array_merge(
-                                       $this->getDefaultQueryFields(),
-                                       $this->getCommentQueryFields(),
-                                       $this->getActorQueryFields(),
-                                       [
-                                               'page_namespace',
-                                               'page_title',
-                                               'page_id',
-                                               'page_latest',
-                                               'page_is_redirect',
-                                               'page_len',
-                                       ]
-                               ),
-                               'joins' => [
-                                       'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
-                               ],
-                       ]
-               ];
-               yield [
-                       false,
-                       [ 'user' ],
-                       [
-                               'tables' => [ 'revision', 'user' ],
-                               'fields' => array_merge(
-                                       $this->getDefaultQueryFields(),
-                                       $this->getCommentQueryFields(),
-                                       $this->getActorQueryFields(),
-                                       [
-                                               'user_name',
-                                       ]
-                               ),
-                               'joins' => [
-                                       'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
-                               ],
-                       ]
-               ];
-               yield [
-                       false,
-                       [ 'text' ],
-                       [
-                               'tables' => [ 'revision', 'text' ],
-                               'fields' => array_merge(
-                                       $this->getDefaultQueryFields(),
-                                       $this->getCommentQueryFields(),
-                                       $this->getActorQueryFields(),
-                                       [
-                                               'old_text',
-                                               'old_flags',
-                                       ]
-                               ),
-                               'joins' => [
-                                       'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ],
-                               ],
-                       ]
-               ];
-               yield [
-                       true,
-                       [ 'page', 'user', 'text' ],
-                       [
-                               'tables' => [ 'revision', 'page', 'user', 'text' ],
-                               'fields' => array_merge(
-                                       $this->getDefaultQueryFields(),
-                                       $this->getCommentQueryFields(),
-                                       $this->getActorQueryFields(),
-                                       $this->getContentHandlerQueryFields(),
-                                       [
-                                               'page_namespace',
-                                               'page_title',
-                                               'page_id',
-                                               'page_latest',
-                                               'page_is_redirect',
-                                               'page_len',
-                                               'user_name',
-                                               'old_text',
-                                               'old_flags',
-                                       ]
-                               ),
-                               'joins' => [
-                                       'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ],
-                                       'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ],
-                                       'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ],
-                               ],
-                       ]
+                       // ContentHandlerUseDB can be true of false pre migration
+                       [ false, MIGRATION_OLD, false ],
+                       [ true, MIGRATION_OLD, false ],
+                       // During migration it can not be false
+                       [ false, MIGRATION_WRITE_BOTH, true ],
+                       // But it can be true
+                       [ true, MIGRATION_WRITE_BOTH, false ],
                ];
        }
 
        /**
-        * @dataProvider provideGetQueryInfo
-        * @covers \MediaWiki\Storage\RevisionStore::getQueryInfo
+        * @dataProvider provideSetContentHandlerUseDB
+        * @covers \MediaWiki\Storage\RevisionStore::getContentHandlerUseDB
+        * @covers \MediaWiki\Storage\RevisionStore::setContentHandlerUseDB
         */
-       public function testGetQueryInfo( $contentHandlerUseDb, $options, $expected ) {
-               $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
-               $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD );
-               $this->overrideMwServices();
-               $store = $this->getRevisionStore();
-               $store->setContentHandlerUseDB( $contentHandlerUseDb );
-               $this->assertEquals( $expected, $store->getQueryInfo( $options ) );
+       public function testSetContentHandlerUseDB( $contentHandlerDb, $migrationMode, $expectedFail ) {
+               if ( $expectedFail ) {
+                       $this->setExpectedException( MWException::class );
+               }
+
+               $store = new RevisionStore(
+                       $this->getMockLoadBalancer(),
+                       $this->getMockSqlBlobStore(),
+                       $this->getHashWANObjectCache(),
+                       $this->getMockCommentStore(),
+                       MediaWikiServices::getInstance()->getContentModelStore(),
+                       MediaWikiServices::getInstance()->getSlotRoleStore(),
+                       $migrationMode,
+                       MediaWikiServices::getInstance()->getActorMigration()
+               );
+
+               $store->setContentHandlerUseDB( $contentHandlerDb );
+               $this->assertSame( $contentHandlerDb, $store->getContentHandlerUseDB() );
        }
 
        public function testGetTitle_successFromPageId() {
@@ -529,7 +403,6 @@ class RevisionStoreTest extends MediaWikiTestCase {
         * @dataProvider provideNewRevisionFromRow_legacyEncoding_applied
         *
         * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
-        * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
         */
        public function testNewRevisionFromRow_legacyEncoding_applied( $encoding, $locale, $row, $text ) {
                $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
@@ -551,7 +424,6 @@ class RevisionStoreTest extends MediaWikiTestCase {
 
        /**
         * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
-        * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29
         */
        public function testNewRevisionFromRow_legacyEncoding_ignored() {
                $row = [
@@ -608,4 +480,47 @@ class RevisionStoreTest extends MediaWikiTestCase {
                return (object)$row;
        }
 
+       public function provideMigrationConstruction() {
+               return [
+                       [ MIGRATION_OLD, false ],
+                       [ MIGRATION_WRITE_BOTH, false ],
+               ];
+       }
+
+       /**
+        * @covers \MediaWiki\Storage\RevisionStore::__construct
+        * @dataProvider provideMigrationConstruction
+        */
+       public function testMigrationConstruction( $migration, $expectException ) {
+               if ( $expectException ) {
+                       $this->setExpectedException( InvalidArgumentException::class );
+               }
+               $loadBalancer = $this->getMockLoadBalancer();
+               $blobStore = $this->getMockSqlBlobStore();
+               $cache = $this->getHashWANObjectCache();
+               $commentStore = $this->getMockCommentStore();
+               $contentModelStore = MediaWikiServices::getInstance()->getContentModelStore();
+               $slotRoleStore = MediaWikiServices::getInstance()->getSlotRoleStore();
+               $store = new RevisionStore(
+                       $loadBalancer,
+                       $blobStore,
+                       $cache,
+                       $commentStore,
+                       MediaWikiServices::getInstance()->getContentModelStore(),
+                       MediaWikiServices::getInstance()->getSlotRoleStore(),
+                       $migration,
+                       MediaWikiServices::getInstance()->getActorMigration()
+               );
+               if ( !$expectException ) {
+                       $store = TestingAccessWrapper::newFromObject( $store );
+                       $this->assertSame( $loadBalancer, $store->loadBalancer );
+                       $this->assertSame( $blobStore, $store->blobStore );
+                       $this->assertSame( $cache, $store->cache );
+                       $this->assertSame( $commentStore, $store->commentStore );
+                       $this->assertSame( $contentModelStore, $store->contentModelStore );
+                       $this->assertSame( $slotRoleStore, $store->slotRoleStore );
+                       $this->assertSame( $migration, $store->mcrMigrationStage );
+               }
+       }
+
 }
index feeb538..1aae16d 100644 (file)
@@ -36,6 +36,7 @@ class SlotRecordTest extends MediaWikiTestCase {
                $record = new SlotRecord( $row, new WikitextContent( 'A' ) );
 
                $this->assertTrue( $record->hasAddress() );
+               $this->assertTrue( $record->hasContentId() );
                $this->assertTrue( $record->hasRevision() );
                $this->assertTrue( $record->isInherited() );
                $this->assertSame( 'A', $record->getContent()->getNativeData() );
@@ -59,6 +60,9 @@ class SlotRecordTest extends MediaWikiTestCase {
                        },
                        'slot_revision_id' => '2',
                        'slot_origin' => '2',
+                       'slot_content_id' => function () {
+                               return null;
+                       },
                ] );
 
                $content = function () {
@@ -69,6 +73,7 @@ class SlotRecordTest extends MediaWikiTestCase {
 
                $this->assertTrue( $record->hasAddress() );
                $this->assertTrue( $record->hasRevision() );
+               $this->assertFalse( $record->hasContentId() );
                $this->assertFalse( $record->isInherited() );
                $this->assertSame( 'A', $record->getContent()->getNativeData() );
                $this->assertSame( 1, $record->getSize() );
@@ -77,7 +82,6 @@ class SlotRecordTest extends MediaWikiTestCase {
                $this->assertSame( 2, $record->getRevision() );
                $this->assertSame( 2, $record->getRevision() );
                $this->assertSame( 'tt:456', $record->getAddress() );
-               $this->assertSame( 33, $record->getContentId() );
                $this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
                $this->assertSame( 'myRole', $record->getRole() );
        }
@@ -86,8 +90,10 @@ class SlotRecordTest extends MediaWikiTestCase {
                $record = SlotRecord::newUnsaved( 'myRole', new WikitextContent( 'A' ) );
 
                $this->assertFalse( $record->hasAddress() );
+               $this->assertFalse( $record->hasContentId() );
                $this->assertFalse( $record->hasRevision() );
                $this->assertFalse( $record->isInherited() );
+               $this->assertFalse( $record->hasOrigin() );
                $this->assertSame( 'A', $record->getContent()->getNativeData() );
                $this->assertSame( 1, $record->getSize() );
                $this->assertNotNull( $record->getSha1() );
@@ -190,6 +196,7 @@ class SlotRecordTest extends MediaWikiTestCase {
                $this->assertSame( $parent->getAddress(), $inherited->getAddress() );
                $this->assertSame( $parent->getContent(), $inherited->getContent() );
                $this->assertTrue( $inherited->isInherited() );
+               $this->assertTrue( $inherited->hasOrigin() );
                $this->assertFalse( $inherited->hasRevision() );
 
                // make sure we didn't mess with the internal state of $parent
@@ -224,8 +231,10 @@ class SlotRecordTest extends MediaWikiTestCase {
                // and content meta-data.
                $saved = SlotRecord::newSaved( 10, 20, 'theNewAddress', $unsaved );
                $this->assertFalse( $saved->isInherited() );
+               $this->assertTrue( $saved->hasOrigin() );
                $this->assertTrue( $saved->hasRevision() );
                $this->assertTrue( $saved->hasAddress() );
+               $this->assertTrue( $saved->hasContentId() );
                $this->assertSame( 'theNewAddress', $saved->getAddress() );
                $this->assertSame( 20, $saved->getContentId() );
                $this->assertSame( 'A', $saved->getContent()->getNativeData() );
@@ -234,6 +243,7 @@ class SlotRecordTest extends MediaWikiTestCase {
 
                // make sure we didn't mess with the internal state of $unsaved
                $this->assertFalse( $unsaved->hasAddress() );
+               $this->assertFalse( $unsaved->hasContentId() );
                $this->assertFalse( $unsaved->hasRevision() );
        }
 
diff --git a/tests/phpunit/includes/Storage/drop-pre-mcr-fields.sql b/tests/phpunit/includes/Storage/drop-pre-mcr-fields.sql
new file mode 100644 (file)
index 0000000..ddfe756
--- /dev/null
@@ -0,0 +1,3 @@
+ALTER TABLE /*_*/revision DROP COLUMN rev_text_id;
+ALTER TABLE /*_*/revision DROP COLUMN rev_content_model;
+ALTER TABLE /*_*/revision DROP COLUMN rev_content_format;
diff --git a/tests/phpunit/includes/Storage/drop-pre-mcr-fields.sqlite.sql b/tests/phpunit/includes/Storage/drop-pre-mcr-fields.sqlite.sql
new file mode 100644 (file)
index 0000000..ce7a618
--- /dev/null
@@ -0,0 +1,15 @@
+DROP TABLE /*_*/revision;
+
+CREATE TABLE /*_*/revision (
+  rev_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  rev_page INTEGER NOT NULL,
+  rev_comment BLOB NOT NULL,
+  rev_user INTEGER NOT NULL default 0,
+  rev_user_text varchar(255) NOT NULL default '',
+  rev_timestamp blob(14) NOT NULL default '',
+  rev_minor_edit INTEGER NOT NULL default 0,
+  rev_deleted INTEGER NOT NULL default 0,
+  rev_len INTEGER unsigned,
+  rev_parent_id INTEGER default NULL,
+  rev_sha1 varbinary(32) NOT NULL default ''
+) /*$wgDBTableOptions*/;
index 4032b3a..e898c63 100644 (file)
@@ -1,5 +1,7 @@
 <?php
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * @group ContentHandler
  * @group Database
@@ -353,7 +355,7 @@ class TitleMethodsTest extends MediaWikiLangTestCase {
         * @covers Title::clearCaches
         */
        public function testClearCaches() {
-               $linkCache = LinkCache::singleton();
+               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
 
                $title1 = Title::newFromText( 'Foo' );
                $linkCache->addGoodLinkObj( 23, $title1 );
index c81a078..f9ffeae 100644 (file)
@@ -1,5 +1,7 @@
 <?php
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * @group Database
  * @group Title
@@ -674,7 +676,7 @@ class TitleTest extends MediaWikiTestCase {
         */
        public function testExists() {
                $title = Title::makeTitle( NS_PROJECT, 'New page' );
-               $linkCache = LinkCache::singleton();
+               $linkCache = MediaWikiServices::getInstance()->getLinkCache();
 
                $article = new Article( $title );
                $page = $article->getPage();
index b1f4e0c..4f65ae9 100644 (file)
@@ -164,10 +164,14 @@ class ApiBaseTest extends ApiTestCase {
        }
 
        public function testGetTitleOrPageIdPageId() {
+               $page = $this->getExistingTestPage();
                $result = ( new MockApi() )->getTitleOrPageId(
-                       [ 'pageid' => Title::newFromText( 'UTPage' )->getArticleId() ] );
+                       [ 'pageid' => $page->getId() ] );
                $this->assertInstanceOf( WikiPage::class, $result );
-               $this->assertSame( 'UTPage', $result->getTitle()->getPrefixedText() );
+               $this->assertSame(
+                       $page->getTitle()->getPrefixedText(),
+                       $result->getTitle()->getPrefixedText()
+               );
        }
 
        public function testGetTitleOrPageIdInvalidPageId() {
@@ -199,10 +203,11 @@ class ApiBaseTest extends ApiTestCase {
        }
 
        public function testGetTitleFromTitleOrPageIdPageId() {
+               $page = $this->getExistingTestPage();
                $result = ( new MockApi() )->getTitleFromTitleOrPageId(
-                       [ 'pageid' => Title::newFromText( 'UTPage' )->getArticleId() ] );
+                       [ 'pageid' => $page->getId() ] );
                $this->assertInstanceOf( Title::class, $result );
-               $this->assertSame( 'UTPage', $result->getPrefixedText() );
+               $this->assertSame( $page->getTitle()->getPrefixedText(), $result->getPrefixedText() );
        }
 
        public function testGetTitleFromTitleOrPageIdInvalidPageId() {
diff --git a/tests/phpunit/includes/api/ApiQueryPrefixSearchTest.php b/tests/phpunit/includes/api/ApiQueryPrefixSearchTest.php
new file mode 100644 (file)
index 0000000..749f154
--- /dev/null
@@ -0,0 +1,58 @@
+<?php
+
+/**
+ * @group API
+ * @group medium
+ *
+ * @covers ApiQueryPrefixSearch
+ */
+class ApiQueryPrefixSearchTest extends ApiTestCase {
+       const TEST_QUERY = 'unittest';
+
+       public function setUp() {
+               parent::setUp();
+               $this->setMwGlobals( [
+                       'wgSearchType' => MockCompletionSearchEngine::class,
+               ] );
+               MockCompletionSearchEngine::clearMockResults();
+               $results = [];
+               foreach ( range( 0, 10 ) as $i ) {
+                       $title = "Search_Result_$i";
+                       $results[] = $title;
+                       $this->editPage( $title, 'hi there' );
+               }
+               MockCompletionSearchEngine::addMockResults( self::TEST_QUERY, $results );
+       }
+
+       public function offsetContinueProvider() {
+               return [
+                       'no offset' => [ 2, 2, 0, 2 ],
+                       'with offset' => [ 7, 2, 5, 2 ],
+                       'past end, no offset' => [ null, 11, 0, 20 ],
+                       'past end, with offset' => [ null, 5, 6, 10 ],
+               ];
+       }
+
+       /**
+        * @dataProvider offsetContinueProvider
+        */
+       public function testOffsetContinue( $expectedOffset, $expectedResults, $offset, $limit ) {
+               $response = $this->doApiRequest( [
+                       'action' => 'query',
+                       'list' => 'prefixsearch',
+                       'pssearch' => self::TEST_QUERY,
+                       'psoffset' => $offset,
+                       'pslimit' => $limit,
+               ] );
+               $result = $response[0];
+               $this->assertArrayNotHasKey( 'warnings', $result );
+               $suggestions = $result['query']['prefixsearch'];
+               $this->assertCount( $expectedResults, $suggestions );
+               if ( $expectedOffset == null ) {
+                       $this->assertArrayNotHasKey( 'continue', $result );
+               } else {
+                       $this->assertArrayHasKey( 'continue', $result );
+                       $this->assertEquals( $expectedOffset, $result['continue']['psoffset'] );
+               }
+       }
+}
diff --git a/tests/phpunit/includes/api/ApiQuerySearchTest.php b/tests/phpunit/includes/api/ApiQuerySearchTest.php
new file mode 100644 (file)
index 0000000..0700cf7
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+
+/**
+ * @group medium
+ * @covers ApiQuerySearch
+ */
+class ApiQuerySearchTest extends ApiTestCase {
+       public function provideSearchResults() {
+               return [
+                       'empty search result' => [ [], [] ],
+                       'has search results' => [
+                               [ 'Zomg' ],
+                               [ $this->mockResult( 'Zomg' ) ],
+                       ],
+                       'filters broken search results' => [
+                               [ 'A', 'B' ],
+                               [
+                                       $this->mockResult( 'a' ),
+                                       $this->mockResult( 'Zomg' )->setBrokenTitle( true ),
+                                       $this->mockResult( 'b' ),
+                               ],
+                       ],
+                       'filters results with missing revision' => [
+                               [ 'B', 'A' ],
+                               [
+                                       $this->mockResult( 'Zomg' )->setMissingRevision( true ),
+                                       $this->mockResult( 'b' ),
+                                       $this->mockResult( 'a' ),
+                               ],
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideSearchResults
+        */
+       public function testSearchResults( $expect, $hits, array $params = [] ) {
+               MockSearchEngine::addMockResults( 'my query', $hits );
+               list( $response, $request ) = $this->doApiRequest( $params + [
+                       'action' => 'query',
+                       'list' => 'search',
+                       'srsearch' => 'my query',
+               ] );
+               $titles = [];
+               foreach ( $response['query']['search'] as $result ) {
+                       $titles[] = $result['title'];
+               }
+               $this->assertEquals( $expect, $titles );
+       }
+
+       public function provideInterwikiResults() {
+               return [
+                       'empty' => [ [], [] ],
+                       'one wiki response' => [
+                               [ 'utwiki' => [ 'Qwerty' ] ],
+                               [
+                                       SearchResultSet::SECONDARY_RESULTS => [
+                                               'utwiki' => new MockSearchResultSet( [
+                                                       $this->mockResult( 'Qwerty' )->setInterwikiPrefix( 'utwiki' ),
+                                               ] ),
+                                       ],
+                               ]
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideInterwikiResults
+        */
+       public function testInterwikiResults( $expect, $hits, array $params = [] ) {
+               MockSearchEngine::setMockInterwikiResults( $hits );
+               list( $response, $request ) = $this->doApiRequest( $params + [
+                       'action' => 'query',
+                       'list' => 'search',
+                       'srsearch' => 'my query',
+                       'srinterwiki' => true,
+               ] );
+               if ( !$expect ) {
+                       $this->assertArrayNotHasKey( 'interwikisearch', $response['query'] );
+                       return;
+               }
+               $results = [];
+               $this->assertArrayHasKey( 'interwikisearchinfo', $response['query'] );
+               foreach ( $response['query']['interwikisearch'] as $wiki => $wikiResults ) {
+                       $results[$wiki] = [];
+                       foreach ( $wikiResults as $wikiResult ) {
+                               $results[$wiki][] = $wikiResult['title'];
+                       }
+               }
+               $this->assertEquals( $expect, $results );
+       }
+
+       public function setUp() {
+               parent::setUp();
+               MockSearchEngine::clearMockResults();
+               $this->registerMockSearchEngine();
+       }
+
+       private function registerMockSearchEngine() {
+               $this->setMwGlobals( [
+                       'wgSearchType' => MockSearchEngine::class,
+               ] );
+       }
+
+       private function mockResult( $title ) {
+               return MockSearchResult::newFromtitle( Title::newFromText( $title ) );
+       }
+
+}
index 5f59d6f..6532635 100644 (file)
@@ -89,17 +89,14 @@ class ApiQueryWatchlistIntegrationTest extends ApiTestCase {
                User $patrollingUser
        ) {
                $title = Title::newFromLinkTarget( $target );
+               $summary = CommentStoreComment::newUnsavedComment( trim( $summary ) );
                $page = WikiPage::factory( $title );
-               $status = $page->doEditContent(
-                       ContentHandler::makeContent( $content, $title ),
-                       $summary,
-                       0,
-                       false,
-                       $user
-               );
-               /** @var Revision $rev */
-               $rev = $status->value['revision'];
-               $rc = $rev->getRecentChange();
+
+               $updater = $page->newPageUpdater( $user );
+               $updater->setContent( 'main', ContentHandler::makeContent( $content, $title ) );
+               $rev = $updater->saveRevision( $summary );
+
+               $rc = MediaWikiServices::getInstance()->getRevisionStore()->getRecentChange( $rev );
                $rc->doMarkPatrolled( $patrollingUser, false, [] );
        }
 
index 215cdfd..216228a 100644 (file)
@@ -421,7 +421,9 @@ class ChangeTagsTest extends MediaWikiTestCase {
                $this->assertEquals( $expected2, iterator_to_array( $res2, false ) );
 
                $rcId = 124;
-               ChangeTags::updateTags( [ 'tag1', 'tag3' ], [], $rcId );
+               ChangeTags::updateTags( [ 'tag1' ], [], $rcId );
+
+               ChangeTags::updateTags( [ 'tag3' ], [], $rcId );
 
                $dbr = wfGetDB( DB_REPLICA );
 
index 309b7b1..9a1c90f 100644 (file)
@@ -101,7 +101,7 @@ class ContentHandlerTest extends MediaWikiTestCase {
         */
        public function testGetForTitle( $title, $expectedContentModel ) {
                $title = Title::newFromText( $title );
-               LinkCache::singleton()->addBadLinkObj( $title );
+               MediaWikiServices::getInstance()->getLinkCache()->addBadLinkObj( $title );
                $handler = ContentHandler::getForTitle( $title );
                $this->assertEquals( $expectedContentModel, $handler->getModelID() );
        }
@@ -158,7 +158,7 @@ class ContentHandlerTest extends MediaWikiTestCase {
        public function testGetPageLanguage( $title, $expected ) {
                if ( is_string( $title ) ) {
                        $title = Title::newFromText( $title );
-                       LinkCache::singleton()->addBadLinkObj( $title );
+                       MediaWikiServices::getInstance()->getLinkCache()->addBadLinkObj( $title );
                }
 
                $expected = wfGetLangObj( $expected );
@@ -308,7 +308,7 @@ class ContentHandlerTest extends MediaWikiTestCase {
                $expectedModelId, $expectedNativeData, $shouldFail
        ) {
                $title = Title::newFromText( $title );
-               LinkCache::singleton()->addBadLinkObj( $title );
+               MediaWikiServices::getInstance()->getLinkCache()->addBadLinkObj( $title );
                try {
                        $content = ContentHandler::makeContent( $data, $title, $modelId, $format );
 
index fac3486..82ca66a 100644 (file)
@@ -630,35 +630,41 @@ class LBFactoryTest extends MediaWikiTestCase {
                        '1@542#c47dcfb0566e7d7bc110a6128a45c93a',
                        LBFactory::makeCookieValueFromCPIndex( 1, 542, $agentId )
                );
+
                $this->assertSame(
-                       5,
-                       LBFactory::getCPInfoFromCookieValue( "5", $time - 10 )['index'],
+                       null,
+                       LBFactory::getCPInfoFromCookieValue( "5#$agentId", $time - 10 )['index'],
                        'No time set'
                );
                $this->assertSame(
                        null,
-                       LBFactory::getCPInfoFromCookieValue( "0", $time - 10 )['index'],
+                       LBFactory::getCPInfoFromCookieValue( "5@$time", $time - 10 )['index'],
+                       'No agent set'
+               );
+               $this->assertSame(
+                       null,
+                       LBFactory::getCPInfoFromCookieValue( "0@$time#$agentId", $time - 10 )['index'],
                        'Bad index'
                );
 
                $this->assertSame(
                        2,
-                       LBFactory::getCPInfoFromCookieValue( "2@$time", $time - 10 )['index'],
+                       LBFactory::getCPInfoFromCookieValue( "2@$time#$agentId", $time - 10 )['index'],
                        'Fresh'
                );
                $this->assertSame(
                        2,
-                       LBFactory::getCPInfoFromCookieValue( "2@$time", $time + 9 - 10 )['index'],
+                       LBFactory::getCPInfoFromCookieValue( "2@$time#$agentId", $time + 9 - 10 )['index'],
                        'Almost stale'
                );
                $this->assertSame(
                        null,
-                       LBFactory::getCPInfoFromCookieValue( "0@$time", $time + 9 - 10 )['index'],
+                       LBFactory::getCPInfoFromCookieValue( "0@$time#$agentId", $time + 9 - 10 )['index'],
                        'Almost stale; bad index'
                );
                $this->assertSame(
                        null,
-                       LBFactory::getCPInfoFromCookieValue( "2@$time", $time + 11 - 10 )['index'],
+                       LBFactory::getCPInfoFromCookieValue( "2@$time#$agentId", $time + 11 - 10 )['index'],
                        'Stale'
                );
 
@@ -669,7 +675,7 @@ class LBFactoryTest extends MediaWikiTestCase {
                );
                $this->assertSame(
                        null,
-                       LBFactory::getCPInfoFromCookieValue( "5@$time", $time + 11 - 10 )['clientId'],
+                       LBFactory::getCPInfoFromCookieValue( "5@$time#$agentId", $time + 11 - 10 )['clientId'],
                        'Stale (client ID)'
                );
        }
index 6d096c2..c758ba6 100644 (file)
@@ -131,6 +131,11 @@ class LinkRendererTest extends MediaWikiLangTestCase {
                        . '(page does not exist)"><script>evil()</script></a>',
                        $linkRenderer->makeLink( $foobar, new HtmlArmor( '<script>evil()</script>' ) )
                );
+
+               $this->assertEquals(
+                       '<a href="#fragment">fragment</a>',
+                       $linkRenderer->makeLink( Title::newFromText( '#fragment' ) )
+               );
        }
 
        public function testGetLinkClasses() {
index cc20b6b..6a87dfb 100644 (file)
@@ -1,5 +1,11 @@
 <?php
 
+use MediaWiki\Storage\RevisionSlotsUpdate;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @covers WikiPage
+ */
 abstract class WikiPageDbTestBase extends MediaWikiLangTestCase {
 
        private $pagesToDelete;
@@ -30,6 +36,10 @@ abstract class WikiPageDbTestBase extends MediaWikiLangTestCase {
                                'iwlinks' ] );
        }
 
+       protected function addCoreDBData() {
+               // Blank out. This would fail with a modified schema, and we don't need it.
+       }
+
        /**
         * @return int
         */
@@ -97,29 +107,140 @@ abstract class WikiPageDbTestBase extends MediaWikiLangTestCase {
         *
         * @return WikiPage
         */
-       protected function createPage( $page, $text, $model = null ) {
+       protected function createPage( $page, $text, $model = null, $user = null ) {
                if ( is_string( $page ) || $page instanceof Title ) {
                        $page = $this->newPage( $page, $model );
                }
 
                $content = ContentHandler::makeContent( $text, $page->getTitle(), $model );
-               $page->doEditContent( $content, "testing", EDIT_NEW );
+               $page->doEditContent( $content, "testing", EDIT_NEW, false, $user );
 
                return $page;
        }
 
        /**
-        * @covers WikiPage::doEditContent
-        * @covers WikiPage::doModify
-        * @covers WikiPage::doCreate
+        * @covers WikiPage::prepareContentForEdit
+        */
+       public function testPrepareContentForEdit() {
+               $user = $this->getTestUser()->getUser();
+               $sysop = $this->getTestUser( [ 'sysop' ] )->getUser();
+
+               $page = $this->createPage( __METHOD__, __METHOD__, null, $user );
+               $title = $page->getTitle();
+
+               $content = ContentHandler::makeContent(
+                       "[[Lorem ipsum]] dolor sit amet, consetetur sadipscing elitr, sed diam "
+                       . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.",
+                       $title,
+                       CONTENT_MODEL_WIKITEXT
+               );
+               $content2 = ContentHandler::makeContent(
+                       "At vero eos et accusam et justo duo [[dolores]] et ea rebum. "
+                       . "Stet clita kasd [[gubergren]], no sea takimata sanctus est. ~~~~",
+                       $title,
+                       CONTENT_MODEL_WIKITEXT
+               );
+
+               $edit = $page->prepareContentForEdit( $content, null, $user, null, false );
+
+               $this->assertInstanceOf(
+                       ParserOptions::class,
+                       $edit->popts,
+                       "pops"
+               );
+               $this->assertContains( '</a>', $edit->output->getText(), "output" );
+               $this->assertContains(
+                       'consetetur sadipscing elitr',
+                       $edit->output->getText(),
+                       "output"
+               );
+
+               $this->assertTrue( $content->equals( $edit->newContent ), "newContent field" );
+               $this->assertTrue( $content->equals( $edit->pstContent ), "pstContent field" );
+               $this->assertSame( $edit->output, $edit->output, "output field" );
+               $this->assertSame( $edit->popts, $edit->popts, "popts field" );
+               $this->assertSame( null, $edit->revid, "revid field" );
+
+               // Re-using the prepared info if possible
+               $sameEdit = $page->prepareContentForEdit( $content, null, $user, null, false );
+               $this->assertEquals( $edit, $sameEdit, 'equivalent PreparedEdit' );
+               $this->assertSame( $edit->pstContent, $sameEdit->pstContent, 're-use output' );
+               $this->assertSame( $edit->output, $sameEdit->output, 're-use output' );
+
+               // Not re-using the same PreparedEdit if not possible
+               $rev = $page->getRevision();
+               $edit2 = $page->prepareContentForEdit( $content2, null, $user, null, false );
+               $this->assertNotEquals( $edit, $edit2 );
+               $this->assertContains( 'At vero eos', $edit2->pstContent->serialize(), "content" );
+
+               // Check pre-safe transform
+               $this->assertContains( '[[gubergren]]', $edit2->pstContent->serialize() );
+               $this->assertNotContains( '~~~~', $edit2->pstContent->serialize() );
+
+               $edit3 = $page->prepareContentForEdit( $content2, null, $sysop, null, false );
+               $this->assertNotEquals( $edit2, $edit3 );
+
+               // TODO: test with passing revision, then same without revision.
+       }
+
+       /**
         * @covers WikiPage::doEditUpdates
         */
+       public function testDoEditUpdates() {
+               $user = $this->getTestUser()->getUser();
+
+               // NOTE: if site stats get out of whack and drop below 0,
+               // that causes a DB error during tear-down. So bump the
+               // numbers high enough to not drop below 0.
+               $siteStatsUpdate = SiteStatsUpdate::factory(
+                       [ 'edits' => 1000, 'articles' => 1000, 'pages' => 1000 ]
+               );
+               $siteStatsUpdate->doUpdate();
+
+               $page = $this->createPage( __METHOD__, __METHOD__ );
+
+               $revision = new Revision(
+                       [
+                               'id' => 9989,
+                               'page' => $page->getId(),
+                               'title' => $page->getTitle(),
+                               'comment' => __METHOD__,
+                               'minor_edit' => true,
+                               'text' => __METHOD__ . ' [[|foo]][[bar]]', // PST turns [[|foo]] into [[foo]]
+                               'user' => $user->getId(),
+                               'user_text' => $user->getName(),
+                               'timestamp' => '20170707040404',
+                               'content_model' => CONTENT_MODEL_WIKITEXT,
+                               'content_format' => CONTENT_FORMAT_WIKITEXT,
+                       ]
+               );
+
+               $page->doEditUpdates( $revision, $user );
+
+               // TODO: test various options; needs temporary hooks
+
+               $dbr = wfGetDB( DB_REPLICA );
+               $res = $dbr->select( 'pagelinks', '*', [ 'pl_from' => $page->getId() ] );
+               $n = $res->numRows();
+               $res->free();
+
+               $this->assertEquals( 1, $n, 'pagelinks should contain only one link if PST was not applied' );
+       }
+
+       /**
+        * @covers WikiPage::doEditContent
+        * @covers WikiPage::prepareContentForEdit
+        */
        public function testDoEditContent() {
                $this->setMwGlobals( 'wgPageCreationLog', true );
 
                $page = $this->newPage( __METHOD__ );
                $title = $page->getTitle();
 
+               $user1 = $this->getTestUser()->getUser();
+               // Use the confirmed group for user2 to make sure the user is different
+               $user2 = $this->getTestUser( [ 'confirmed' ] )->getUser();
+
                $content = ContentHandler::makeContent(
                        "[[Lorem ipsum]] dolor sit amet, consetetur sadipscing elitr, sed diam "
                                . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.",
@@ -127,7 +248,18 @@ abstract class WikiPageDbTestBase extends MediaWikiLangTestCase {
                        CONTENT_MODEL_WIKITEXT
                );
 
-               $page->doEditContent( $content, "[[testing]] 1" );
+               $status = $page->doEditContent( $content, "[[testing]] 1", EDIT_NEW, false, $user1 );
+
+               $this->assertTrue( $status->isOK(), 'OK' );
+               $this->assertTrue( $status->value['new'], 'new' );
+               $this->assertNotNull( $status->value['revision'], 'revision' );
+               $this->assertSame( $status->value['revision']->getId(), $page->getRevision()->getId() );
+               $this->assertSame( $status->value['revision']->getSha1(), $page->getRevision()->getSha1() );
+               $this->assertTrue( $status->value['revision']->getContent()->equals( $content ), 'equals' );
+
+               $rev = $page->getRevision();
+               $this->assertNotNull( $rev->getRecentChange() );
+               $this->assertSame( $rev->getId(), (int)$rev->getRecentChange()->getAttribute( 'rc_this_oldid' ) );
 
                $id = $page->getId();
 
@@ -158,21 +290,47 @@ abstract class WikiPageDbTestBase extends MediaWikiLangTestCase {
                $retrieved = $page->getContent();
                $this->assertTrue( $content->equals( $retrieved ), 'retrieved content doesn\'t equal original' );
 
+               # ------------------------
+               $page = new WikiPage( $title );
+
+               // try null edit, with a different user
+               $status = $page->doEditContent( $content, 'This changes nothing', EDIT_UPDATE, false, $user2 );
+               $this->assertTrue( $status->isOK(), 'OK' );
+               $this->assertFalse( $status->value['new'], 'new' );
+               $this->assertNull( $status->value['revision'], 'revision' );
+               $this->assertNotNull( $page->getRevision() );
+               $this->assertTrue( $page->getRevision()->getContent()->equals( $content ), 'equals' );
+
                # ------------------------
                $content = ContentHandler::makeContent(
                        "At vero eos et accusam et justo duo [[dolores]] et ea rebum. "
-                               . "Stet clita kasd [[gubergren]], no sea takimata sanctus est.",
+                               . "Stet clita kasd [[gubergren]], no sea takimata sanctus est. ~~~~",
                        $title,
                        CONTENT_MODEL_WIKITEXT
                );
 
-               $page->doEditContent( $content, "testing 2" );
+               $status = $page->doEditContent( $content, "testing 2", EDIT_UPDATE );
+               $this->assertTrue( $status->isOK(), 'OK' );
+               $this->assertFalse( $status->value['new'], 'new' );
+               $this->assertNotNull( $status->value['revision'], 'revision' );
+               $this->assertSame( $status->value['revision']->getId(), $page->getRevision()->getId() );
+               $this->assertSame( $status->value['revision']->getSha1(), $page->getRevision()->getSha1() );
+               $this->assertFalse(
+                       $status->value['revision']->getContent()->equals( $content ),
+                       'not equals (PST must substitute signature)'
+               );
+
+               $rev = $page->getRevision();
+               $this->assertNotNull( $rev->getRecentChange() );
+               $this->assertSame( $rev->getId(), (int)$rev->getRecentChange()->getAttribute( 'rc_this_oldid' ) );
 
                # ------------------------
                $page = new WikiPage( $title );
 
                $retrieved = $page->getContent();
-               $this->assertTrue( $content->equals( $retrieved ), 'retrieved content doesn\'t equal original' );
+               $newText = $retrieved->serialize();
+               $this->assertContains( '[[gubergren]]', $newText, 'New text must replace old text.' );
+               $this->assertNotContains( '~~~~', $newText, 'PST must substitute signature.' );
 
                # ------------------------
                $dbr = wfGetDB( DB_REPLICA );
@@ -1239,6 +1397,44 @@ more stuff
                $this->assertEquals( WikiPage::class, get_class( $page ) );
        }
 
+       /**
+        * @covers WikiPage::loadPageData
+        * @covers WikiPage::wasLoadedFrom
+        */
+       public function testLoadPageData() {
+               $title = Title::makeTitle( NS_MAIN, 'SomePage' );
+               $page = WikiPage::factory( $title );
+
+               $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
+               $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
+               $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
+               $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
+
+               $page->loadPageData( IDBAccessObject::READ_NORMAL );
+               $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
+               $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
+               $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
+               $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
+
+               $page->loadPageData( IDBAccessObject::READ_LATEST );
+               $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
+               $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
+               $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
+               $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
+
+               $page->loadPageData( IDBAccessObject::READ_LOCKING );
+               $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
+               $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
+               $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
+               $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
+
+               $page->loadPageData( IDBAccessObject::READ_EXCLUSIVE );
+               $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
+               $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
+               $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
+               $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
+       }
+
        /**
         * @dataProvider provideCommentMigrationOnDeletion
         *
@@ -2095,4 +2291,89 @@ more stuff
                );
        }
 
+       /**
+        * @covers WikiPage::newPageUpdater
+        * @covers WikiPage::getDerivedDataUpdater
+        */
+       public function testNewPageUpdater() {
+               $user = $this->getTestUser()->getUser();
+               $page = $this->newPage( __METHOD__, __METHOD__ );
+
+               /** @var Content $content */
+               $content = $this->getMockBuilder( WikitextContent::class )
+                       ->setConstructorArgs( [ 'Hello World' ] )
+                       ->setMethods( [ 'getParserOutput' ] )
+                       ->getMock();
+               $content->expects( $this->once() )
+                       ->method( 'getParserOutput' )
+                       ->willReturn( new ParserOutput( 'HTML' ) );
+
+               $updater = $page->newPageUpdater( $user );
+               $updater->setContent( 'main', $content );
+               $revision = $updater->saveRevision(
+                       CommentStoreComment::newUnsavedComment( 'test' ),
+                       EDIT_NEW
+               );
+
+               $this->assertSame( $revision->getId(), $page->getLatest() );
+       }
+
+       /**
+        * @covers WikiPage::newPageUpdater
+        * @covers WikiPage::getDerivedDataUpdater
+        */
+       public function testGetDerivedDataUpdater() {
+               $admin = $this->getTestSysop()->getUser();
+
+               /** @var object $page */
+               $page = $this->createPage( __METHOD__, __METHOD__ );
+               $page = TestingAccessWrapper::newFromObject( $page );
+
+               $revision = $page->getRevision()->getRevisionRecord();
+               $user = $revision->getUser();
+
+               $slotsUpdate = new RevisionSlotsUpdate();
+               $slotsUpdate->modifyContent( 'main', new WikitextContent( 'Hello World' ) );
+
+               // get a virgin updater
+               $updater1 = $page->getDerivedDataUpdater( $user );
+               $this->assertFalse( $updater1->isUpdatePrepared() );
+
+               $updater1->prepareUpdate( $revision );
+
+               // Re-use updater with same revision or content
+               $this->assertSame( $updater1, $page->getDerivedDataUpdater( $user, $revision ) );
+
+               $slotsUpdate = RevisionSlotsUpdate::newFromContent(
+                       [ 'main' => $revision->getContent( 'main' ) ]
+               );
+               $this->assertSame( $updater1, $page->getDerivedDataUpdater( $user, null, $slotsUpdate ) );
+
+               // Don't re-use with different user
+               $updater2a = $page->getDerivedDataUpdater( $admin, null, $slotsUpdate );
+               $updater2a->prepareContent( $admin, $slotsUpdate, false );
+
+               $updater2b = $page->getDerivedDataUpdater( $user, null, $slotsUpdate );
+               $updater2b->prepareContent( $user, $slotsUpdate, false );
+               $this->assertNotSame( $updater2a, $updater2b );
+
+               // Don't re-use with different content
+               $updater3 = $page->getDerivedDataUpdater( $admin, null, $slotsUpdate );
+               $updater3->prepareUpdate( $revision );
+               $this->assertNotSame( $updater2b, $updater3 );
+
+               // Don't re-use if no context given
+               $updater4 = $page->getDerivedDataUpdater( $admin );
+               $updater4->prepareUpdate( $revision );
+               $this->assertNotSame( $updater3, $updater4 );
+
+               // Don't re-use if AGAIN no context given
+               $updater5 = $page->getDerivedDataUpdater( $admin );
+               $this->assertNotSame( $updater4, $updater5 );
+
+               // Don't re-use cached "virgin" unprepared updater
+               $updater6 = $page->getDerivedDataUpdater( $admin, $revision );
+               $this->assertNotSame( $updater5, $updater6 );
+       }
+
 }
diff --git a/tests/phpunit/includes/page/WikiPageMcrDbTest.php b/tests/phpunit/includes/page/WikiPageMcrDbTest.php
new file mode 100644 (file)
index 0000000..02567f8
--- /dev/null
@@ -0,0 +1,28 @@
+<?php
+
+use MediaWiki\Tests\Storage\McrSchemaOverride;
+
+/**
+ * Tests WikiPage against the MCR DB schema after schema migration.
+ *
+ * @covers WikiPage
+ *
+ * @group WikiPage
+ * @group Storage
+ * @group ContentHandler
+ * @group Database
+ * @group medium
+ */
+class WikiPageMcrDbTest extends WikiPageDbTestBase {
+
+       use McrSchemaOverride;
+
+       public function setUp() {
+               parent::setUp();
+       }
+
+       protected function getContentHandlerUseDB() {
+               return true;
+       }
+
+}
diff --git a/tests/phpunit/includes/page/WikiPageMcrWriteBothDbTest.php b/tests/phpunit/includes/page/WikiPageMcrWriteBothDbTest.php
new file mode 100644 (file)
index 0000000..78bbfa7
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+use MediaWiki\Tests\Storage\McrWriteBothSchemaOverride;
+
+/**
+ * Tests WikiPage against the intermediate MCR DB schema for use during schema migration.
+ *
+ * @covers WikiPage
+ *
+ * @group WikiPage
+ * @group Storage
+ * @group ContentHandler
+ * @group Database
+ * @group medium
+ */
+class WikiPageMcrWriteBothDbTest extends WikiPageDbTestBase {
+
+       use McrWriteBothSchemaOverride;
+
+       protected function getContentHandlerUseDB() {
+               return true;
+       }
+
+}
index fa4d804..58e6d7d 100644 (file)
@@ -26,8 +26,12 @@ class MessageBlobStoreTest extends PHPUnit\Framework\TestCase {
                $this->wanCache->expects( $this->any() )
                        ->method( 'makePurgeValue' )
                        ->will( $this->returnCallback( function ( $timestamp, $holdoff ) {
-                               // Disable holdoff as it messes with testing
-                               return WANObjectCache::PURGE_VAL_PREFIX . (float)$timestamp . ':0';
+                               // Disable holdoff as it messes with testing. Aside from a 0-second holdoff,
+                               // make sure that "time" passes between getMulti() check init and the set()
+                               // in recacheMessageBlob(). This especially matters for Windows clocks.
+                               $ts = (float)$timestamp - 0.0001;
+
+                               return WANObjectCache::PURGE_VAL_PREFIX . $ts . ':0';
                        } ) );
        }
 
@@ -197,12 +201,16 @@ class MessageBlobStoreTest extends PHPUnit\Framework\TestCase {
                        ->method( 'fetchMessage' )
                        ->will( $this->onConsecutiveCalls( 'First', 'Second' ) );
 
+               $now = microtime( true );
+               $this->wanCache->setMockTime( $now );
+
                $blob = $blobStore->getBlob( $module, 'en' );
                $this->assertEquals( '{"example":"First"}', $blob, 'Generated blob' );
 
                $blob = $blobStore->getBlob( $module, 'en' );
                $this->assertEquals( '{"example":"First"}', $blob, 'Cache-hit' );
 
+               $now += 1;
                $blobStore->clear();
 
                $blob = $blobStore->getBlob( $module, 'en' );
index 3f59295..83df61a 100644 (file)
@@ -45,6 +45,9 @@ class SearchEnginePrefixTest extends MediaWikiLangTestCase {
                $this->insertPage( 'Talk:Example' );
 
                $this->insertPage( 'User:Example' );
+               $this->insertPage( 'Barcelona' );
+               $this->insertPage( 'Barbara' );
+               $this->insertPage( 'External' );
        }
 
        protected function setUp() {
@@ -238,7 +241,7 @@ class SearchEnginePrefixTest extends MediaWikiLangTestCase {
                                ],
                        ] ],
                        [ [
-                               'Exact match not on top (T72958)',
+                               'Exact match not in first result should be moved to the first result (T72958)',
                                'provision' => [
                                        'Barcelona',
                                        'Bar',
@@ -252,7 +255,7 @@ class SearchEnginePrefixTest extends MediaWikiLangTestCase {
                                ],
                        ] ],
                        [ [
-                               'Exact match missing (T72958)',
+                               'Exact match missing from results should be added as first result (T72958)',
                                'provision' => [
                                        'Barcelona',
                                        'Barbara',
@@ -266,7 +269,7 @@ class SearchEnginePrefixTest extends MediaWikiLangTestCase {
                                ],
                        ] ],
                        [ [
-                               'Exact match missing and not existing',
+                               'Exact match missing and not existing pages should be dropped',
                                'provision' => [
                                        'Exile',
                                        'Exist',
@@ -274,8 +277,6 @@ class SearchEnginePrefixTest extends MediaWikiLangTestCase {
                                ],
                                'query' => 'Ex',
                                'results' => [
-                                       'Exile',
-                                       'Exist',
                                        'External',
                                ],
                        ] ],
@@ -329,6 +330,21 @@ class SearchEnginePrefixTest extends MediaWikiLangTestCase {
                                        'Redirect test',
                                ],
                        ] ],
+                       [ [
+                               "Extra results must not be returned",
+                               'provision' => [
+                                       'Example',
+                                       'Example Bar',
+                                       'Example Foo',
+                                       'Example Foo/Bar'
+                               ],
+                               'query' => 'foo',
+                               'results' => [
+                                       'Example',
+                                       'Example Bar',
+                                       'Example Foo',
+                               ],
+                       ] ],
                ];
        }
 
@@ -337,16 +353,7 @@ class SearchEnginePrefixTest extends MediaWikiLangTestCase {
         * @covers PrefixSearch::searchBackend
         */
        public function testSearchBackend( array $case ) {
-               $search = $stub = $this->getMockBuilder( SearchEngine::class )
-                       ->setMethods( [ 'completionSearchBackend' ] )->getMock();
-
-               $return = SearchSuggestionSet::fromStrings( $case['provision'] );
-
-               $search->expects( $this->any() )
-                       ->method( 'completionSearchBackend' )
-                       ->will( $this->returnValue( $return ) );
-
-               $search->setLimitOffset( 3 );
+               $search = $this->mockSearchWithResults( $case['provision'] );
                $results = $search->completionSearch( $case['query'] );
 
                $results = $results->map( function ( SearchSuggestion $s ) {
@@ -359,4 +366,43 @@ class SearchEnginePrefixTest extends MediaWikiLangTestCase {
                        $case[0]
                );
        }
+
+       public function paginationProvider() {
+               $res = [ 'Example', 'Example Bar', 'Example Foo', 'Example Foo/Bar' ];
+               return [
+                       'With less than requested results no pagination' => [
+                               false, array_slice( $res, 0, 2 ),
+                       ],
+                       'With same as requested results no pagination' => [
+                               false, array_slice( $res, 0, 3 ),
+                       ],
+                       'With extra result returned offer pagination' => [
+                               true, $res,
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider paginationProvider
+        */
+       public function testPagination( $hasMoreResults, $provision ) {
+               $search = $this->mockSearchWithResults( $provision );
+               $results = $search->completionSearch( 'irrelevant' );
+
+               $this->assertEquals( $hasMoreResults, $results->hasMoreResults() );
+       }
+
+       private function mockSearchWithResults( $titleStrings, $limit = 3 ) {
+               $search = $stub = $this->getMockBuilder( SearchEngine::class )
+                       ->setMethods( [ 'completionSearchBackend' ] )->getMock();
+
+               $return = SearchSuggestionSet::fromStrings( $titleStrings );
+
+               $search->expects( $this->any() )
+                       ->method( 'completionSearchBackend' )
+                       ->will( $this->returnValue( $return ) );
+
+               $search->setLimitOffset( $limit );
+               return $search;
+       }
 }
index e807776..5884d19 100644 (file)
@@ -84,10 +84,8 @@ class SearchEngineTest extends MediaWikiLangTestCase {
                $this->assertTrue( is_object( $results ) );
 
                $matches = [];
-               $row = $results->next();
-               while ( $row ) {
+               foreach ( $results as $row ) {
                        $matches[] = $row->getTitle()->getPrefixedText();
-                       $row = $results->next();
                }
                $results->free();
                # Search is not guaranteed to return results in a certain order;
@@ -173,7 +171,7 @@ class SearchEngineTest extends MediaWikiLangTestCase {
        public function testPhraseSearchHighlight() {
                $phrase = "smithee is one who smiths";
                $res = $this->search->searchText( "\"$phrase\"" );
-               $match = $res->next();
+               $match = $res->getIterator()->current();
                $snippet = "A <span class='searchmatch'>" . $phrase . "</span>";
                $this->assertStringStartsWith( $snippet,
                        $match->getTextSnippet( $res->termMatches() ),
@@ -277,7 +275,7 @@ class SearchEngineTest extends MediaWikiLangTestCase {
                $this->mergeMwGlobalArrayValue( 'wgHooks',
                        [ 'SearchResultsAugment' => [ [ $this, 'addAugmentors' ] ] ] );
                $this->search->augmentSearchResults( $resultSet );
-               for ( $result = $resultSet->next(); $result; $result = $resultSet->next() ) {
+               foreach ( $resultSet as $result ) {
                        $id = $result->getTitle()->getArticleID();
                        $augmentData = "Result:$id:" . $result->getTitle()->getText();
                        $augmentData2 = "Result2:$id:" . $result->getTitle()->getText();
@@ -292,11 +290,10 @@ class SearchEngineTest extends MediaWikiLangTestCase {
                        ->method( 'augmentAll' )
                        ->willReturnCallback( function ( SearchResultSet $resultSet ) {
                                $data = [];
-                               for ( $result = $resultSet->next(); $result; $result = $resultSet->next() ) {
+                               foreach ( $resultSet as $result ) {
                                        $id = $result->getTitle()->getArticleID();
                                        $data[$id] = "Result:$id:" . $result->getTitle()->getText();
                                }
-                               $resultSet->rewind();
                                return $data;
                        } );
                $setAugmentors['testSet'] = $setAugmentor;
@@ -310,4 +307,37 @@ class SearchEngineTest extends MediaWikiLangTestCase {
                        } );
                $rowAugmentors['testRow'] = $rowAugmentor;
        }
+
+       public function testFiltersMissing() {
+               $availableResults = [];
+               foreach ( range( 0, 11 ) as $i ) {
+                       $title = "Search_Result_$i";
+                       $availableResults[] = $title;
+                       // pages not created must be filtered
+                       if ( $i % 2 == 0 ) {
+                               $this->editPage( $title );
+                       }
+               }
+               MockCompletionSearchEngine::addMockResults( 'foo', $availableResults );
+
+               $engine = new MockCompletionSearchEngine();
+               $engine->setLimitOffset( 10, 0 );
+               $results = $engine->completionSearch( 'foo' );
+               $this->assertEquals( 5, $results->getSize() );
+               $this->assertTrue( $results->hasMoreResults() );
+
+               $engine->setLimitOffset( 10, 10 );
+               $results = $engine->completionSearch( 'foo' );
+               $this->assertEquals( 1, $results->getSize() );
+               $this->assertFalse( $results->hasMoreResults() );
+       }
+
+       private function editPage( $title ) {
+               $page = WikiPage::factory( Title::newFromText( $title ) );
+               $page->doEditContent(
+                       new WikitextContent( 'UTContent' ),
+                       'UTPageSummary',
+                       EDIT_NEW | EDIT_SUPPRESS_RC
+               );
+       }
 }
diff --git a/tests/phpunit/includes/search/SearchNearMatchResultSetTest.php b/tests/phpunit/includes/search/SearchNearMatchResultSetTest.php
new file mode 100644 (file)
index 0000000..67493c4
--- /dev/null
@@ -0,0 +1,15 @@
+<?php
+
+class SearchNearMatchResultSetTest extends PHPUnit\Framework\TestCase {
+       /**
+        * @covers SearchNearMatchResultSet::__construct
+        * @covers SearchNearMatchResultSet::numRows
+        */
+       public function testNumRows() {
+               $resultSet = new SearchNearMatchResultSet( null );
+               $this->assertEquals( 0, $resultSet->numRows() );
+
+               $resultSet = new SearchNearMatchResultSet( Title::newMainPage() );
+               $this->assertEquals( 1, $resultSet->numRows() );
+       }
+}
diff --git a/tests/phpunit/includes/search/SearchResultSetTest.php b/tests/phpunit/includes/search/SearchResultSetTest.php
new file mode 100644 (file)
index 0000000..26a0672
--- /dev/null
@@ -0,0 +1,61 @@
+<?php
+
+class SearchResultSetTest extends MediaWikiTestCase {
+       /**
+        * @covers SearchResultSet::getIterator
+        * @covers SearchResultSet::next
+        * @covers SearchResultSet::rewind
+        */
+       public function testIterate() {
+               $result = SearchResult::newFromTitle( Title::newMainPage() );
+               $resultSet = new MockSearchResultSet( [ $result ] );
+               $this->assertEquals( 1, $resultSet->numRows() );
+               $count = 0;
+               foreach ( $resultSet as $iterResult ) {
+                       $this->assertEquals( $result, $iterResult );
+                       $count++;
+               }
+               $this->assertEquals( 1, $count );
+
+               $this->hideDeprecated( 'SearchResultSet::rewind' );
+               $this->hideDeprecated( 'SearchResultSet::next' );
+               $resultSet->rewind();
+               $count = 0;
+               while ( false !== ( $iterResult = $resultSet->next() ) ) {
+                       $this->assertEquals( $result, $iterResult );
+                       $count++;
+               }
+               $this->assertEquals( 1, $count );
+       }
+
+       /**
+        * @covers SearchResultSet::augmentResult
+        * @covers SearchResultSet::setAugmentedData
+        */
+       public function testDelayedResultAugment() {
+               $result = SearchResult::newFromTitle( Title::newMainPage() );
+               $resultSet = new MockSearchResultSet( [ $result ] );
+               $resultSet->augmentResult( $result );
+               $this->assertEquals( [], $result->getExtensionData() );
+               $resultSet->setAugmentedData( 'foo', [
+                       $result->getTitle()->getArticleID() => 'bar'
+               ] );
+               $this->assertEquals( [ 'foo' => 'bar' ], $result->getExtensionData() );
+       }
+
+       /**
+        * @covers SearchResultSet::shrink
+        * @covers SearchResultSet::count
+        * @covers SearchResultSet::hasMoreResults
+        */
+       public function testHasMoreResults() {
+               $result = SearchResult::newFromTitle( Title::newMainPage() );
+               $resultSet = new MockSearchResultSet( array_fill( 0, 3, $result ) );
+               $this->assertEquals( 3, count( $resultSet ) );
+               $this->assertFalse( $resultSet->hasMoreResults() );
+               $resultSet->shrink( 3 );
+               $this->assertFalse( $resultSet->hasMoreResults() );
+               $resultSet->shrink( 2 );
+               $this->assertTrue( $resultSet->hasMoreResults() );
+       }
+}
diff --git a/tests/phpunit/includes/search/SearchResultTest.php b/tests/phpunit/includes/search/SearchResultTest.php
new file mode 100644 (file)
index 0000000..0e1e24c
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+
+class SearchResultTest extends MediawikiTestCase {
+       /**
+        * @covers SearchResult::getExtensionData
+        * @covers SearchResult::setExtensionData
+        */
+       public function testExtensionData() {
+               $result = SearchResult::newFromTitle( Title::newMainPage() );
+               $this->assertEquals( [], $result->getExtensionData(), 'starts empty' );
+
+               $data = [ 'hello' => 'world' ];
+               $result->setExtensionData( function () use ( &$data ) {
+                       return $data;
+               } );
+               $this->assertEquals( $data, $result->getExtensionData(), 'can set extension data' );
+               $data['this'] = 'that';
+               $this->assertEquals( $data, $result->getExtensionData(), 'refetches from callback' );
+       }
+
+       /**
+        * @covers SearchResult::getExtensionData
+        * @covers SearchResult::setExtensionData
+        */
+       public function testExtensionDataArrayBC() {
+               $result = SearchResult::newFromTitle( Title::newMainPage() );
+               $data = [ 'hello' => 'world' ];
+               $this->hideDeprecated( 'SearchResult::setExtensionData with array argument' );
+               $this->assertEquals( [], $result->getExtensionData(), 'starts empty' );
+               $result->setExtensionData( $data );
+               $this->assertEquals( $data, $result->getExtensionData(), 'can set extension data' );
+               $data['this'] = 'that';
+               $this->assertNotEquals( $data, $result->getExtensionData(), 'shouldnt hold any reference' );
+
+               $result->setExtensionData( $data );
+               $this->assertEquals( $data, $result->getExtensionData(), 'can replace extension data' );
+       }
+}
index f0a5726..196321c 100644 (file)
@@ -262,8 +262,8 @@ class SpecialSearchTestMockResultSet extends SearchResultSet {
                $this->containedSyntax = $containedSyntax;
        }
 
-       public function numRows() {
-               return count( $this->results );
+       public function expandResults() {
+               return $this->results;
        }
 
        public function getTotalHits() {
index 3eb6abd..294bd80 100644 (file)
@@ -4,6 +4,7 @@ define( 'NS_UNITTEST', 5600 );
 define( 'NS_UNITTEST_TALK', 5601 );
 
 use MediaWiki\MediaWikiServices;
+use MediaWiki\User\UserIdentityValue;
 use Wikimedia\TestingAccessWrapper;
 
 /**
@@ -1149,6 +1150,40 @@ class UserTest extends MediaWikiTestCase {
                }
        }
 
+       /**
+        * @covers User::newFromIdentity
+        */
+       public function testNewFromIdentity() {
+               // Registered user
+               $user = $this->getTestUser()->getUser();
+
+               $this->assertSame( $user, User::newFromIdentity( $user ) );
+
+               // ID only
+               $identity = new UserIdentityValue( $user->getId(), '', 0 );
+               $result = User::newFromIdentity( $identity );
+               $this->assertInstanceOf( User::class, $result );
+               $this->assertSame( $user->getId(), $result->getId(), 'ID' );
+               $this->assertSame( $user->getName(), $result->getName(), 'Name' );
+               $this->assertSame( $user->getActorId(), $result->getActorId(), 'Actor' );
+
+               // Name only
+               $identity = new UserIdentityValue( 0, $user->getName(), 0 );
+               $result = User::newFromIdentity( $identity );
+               $this->assertInstanceOf( User::class, $result );
+               $this->assertSame( $user->getId(), $result->getId(), 'ID' );
+               $this->assertSame( $user->getName(), $result->getName(), 'Name' );
+               $this->assertSame( $user->getActorId(), $result->getActorId(), 'Actor' );
+
+               // Actor only
+               $identity = new UserIdentityValue( 0, '', $user->getActorId() );
+               $result = User::newFromIdentity( $identity );
+               $this->assertInstanceOf( User::class, $result );
+               $this->assertSame( $user->getId(), $result->getId(), 'ID' );
+               $this->assertSame( $user->getName(), $result->getName(), 'Name' );
+               $this->assertSame( $user->getActorId(), $result->getActorId(), 'Actor' );
+       }
+
        /**
         * @covers User::getBlockedStatus
         * @covers User::getBlock
index 0bb6a4d..d406c88 100644 (file)
@@ -7,12 +7,14 @@
  * @group SpecialPageAliases
  * @group SystemTest
  * @group medium
+ * @todo This should be a structure test
  *
  * @author Katie Filbert < aude.wiki@gmail.com >
  */
 class SpecialPageAliasTest extends MediaWikiTestCase {
 
        /**
+        * @coversNothing
         * @dataProvider validSpecialPageAliasesProvider
         */
        public function testValidSpecialPageAliases( $code, $specialPageAliases ) {
diff --git a/tests/phpunit/maintenance/populateChangeTagDefTest.php b/tests/phpunit/maintenance/populateChangeTagDefTest.php
new file mode 100644 (file)
index 0000000..719b46b
--- /dev/null
@@ -0,0 +1,286 @@
+<?php
+
+namespace MediaWiki\Tests\Maintenance;
+
+use PopulateChangeTagDef;
+
+/**
+ * @group Database
+ * @covers PopulateChangeTagDef
+ */
+class PopulateChangeTagDefTest extends MaintenanceBaseTestCase {
+
+       public function getMaintenanceClass() {
+               return PopulateChangeTagDef::class;
+       }
+
+       public function setUp() {
+               parent::setUp();
+               $this->tablesUsed = [ 'change_tag', 'change_tag_def' ];
+
+               $this->cleanChangeTagTables();
+               $this->insertChangeTagData();
+       }
+
+       private function cleanChangeTagTables() {
+               wfGetDB( DB_MASTER )->delete( 'change_tag', '*' );
+               wfGetDB( DB_MASTER )->delete( 'change_tag_def', '*' );
+       }
+
+       private function insertChangeTagData() {
+               $changeTags = [];
+
+               $changeTags[] = [
+                       'ct_rc_id' => 1234,
+                       'ct_tag' => 'One Tag',
+               ];
+
+               $changeTags[] = [
+                       'ct_rc_id' => 1235,
+                       'ct_tag' => 'Two Tags',
+               ];
+
+               $changeTags[] = [
+                       'ct_log_id' => 1236,
+                       'ct_tag' => 'Two Tags',
+               ];
+
+               $changeTags[] = [
+                       'ct_rev_id' => 1237,
+                       'ct_tag' => 'Three Tags',
+               ];
+
+               $changeTags[] = [
+                       'ct_rc_id' => 1238,
+                       'ct_tag' => 'Three Tags',
+               ];
+
+               $changeTags[] = [
+                       'ct_log_id' => 1239,
+                       'ct_tag' => 'Three Tags',
+               ];
+
+               wfGetDB( DB_MASTER )->insert( 'change_tag', $changeTags );
+       }
+
+       public function testRun() {
+               $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_WRITE_BOTH );
+               $this->maintenance->loadWithArgv( [ '--sleep', '0' ] );
+
+               $this->maintenance->execute();
+
+               $changeTagDefRows = [
+                       (object)[
+                               'ctd_name' => 'One Tag',
+                               'ctd_count' => 1,
+                       ],
+                       (object)[
+                               'ctd_name' => 'Two Tags',
+                               'ctd_count' => 2,
+                       ],
+                       (object)[
+                               'ctd_name' => 'Three Tags',
+                               'ctd_count' => 3,
+                       ],
+               ];
+
+               $actualChangeTagDefs = wfGetDB( DB_REPLICA )->select(
+                       [ 'change_tag_def' ],
+                       [ 'ctd_name', 'ctd_count' ],
+                       [],
+                       __METHOD__,
+                       [ 'ORDER BY' => 'ctd_count' ]
+               );
+
+               $this->assertEquals( $changeTagDefRows, iterator_to_array( $actualChangeTagDefs, false ) );
+
+               // Check if change_tag is also backpopulated
+               $actualChangeTags = wfGetDB( DB_REPLICA )->select(
+                       [ 'change_tag', 'change_tag_def' ],
+                       [ 'ct_tag', 'ct_tag_id', 'ctd_count' ],
+                       [],
+                       __METHOD__,
+                       [],
+                       [ 'change_tag_def' => [ 'LEFT JOIN', 'ct_tag_id=ctd_id' ] ]
+               );
+               $mapping = [
+                       'One Tag' => 1,
+                       'Two Tags' => 2,
+                       'Three Tags' => 3
+               ];
+               foreach ( $actualChangeTags as $row ) {
+                       $this->assertNotNull( $row->ct_tag_id );
+                       $this->assertEquals( $row->ctd_count, $mapping[$row->ct_tag] );
+               }
+       }
+
+       public function testRunUpdateHitCountMigrationNew() {
+               $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_NEW );
+               $changeTagDefBadRows = [
+                       [
+                               'ctd_name' => 'One Tag',
+                               'ctd_user_defined' => 0,
+                               'ctd_count' => 50,
+                       ],
+                       [
+                               'ctd_name' => 'Two Tags',
+                               'ctd_user_defined' => 0,
+                               'ctd_count' => 4,
+                       ],
+                       [
+                               'ctd_name' => 'Three Tags',
+                               'ctd_user_defined' => 0,
+                               'ctd_count' => 3,
+                       ],
+               ];
+               wfGetDB( DB_MASTER )->insert(
+                       'change_tag_def',
+                       $changeTagDefBadRows
+               );
+
+               $mapping = [
+                       'One Tag' => 1,
+                       'Two Tags' => 2,
+                       'Three Tags' => 3
+               ];
+               foreach ( $mapping as $tagName => $tagId ) {
+                       wfGetDB( DB_MASTER )->update(
+                               'change_tag',
+                               [ 'ct_tag_id' => $tagId ],
+                               [ 'ct_tag' => $tagName ]
+                       );
+               }
+
+               $this->maintenance->loadWithArgv( [ '--sleep', '0' ] );
+
+               $this->maintenance->execute();
+
+               $changeTagDefRows = [
+                       (object)[
+                               'ctd_name' => 'One Tag',
+                               'ctd_count' => 1,
+                       ],
+                       (object)[
+                               'ctd_name' => 'Two Tags',
+                               'ctd_count' => 2,
+                       ],
+                       (object)[
+                               'ctd_name' => 'Three Tags',
+                               'ctd_count' => 3,
+                       ],
+               ];
+
+               $actualChangeTagDefs = wfGetDB( DB_REPLICA )->select(
+                       [ 'change_tag_def' ],
+                       [ 'ctd_name', 'ctd_count' ],
+                       [],
+                       __METHOD__,
+                       [ 'ORDER BY' => 'ctd_count' ]
+               );
+
+               $this->assertEquals( $changeTagDefRows, iterator_to_array( $actualChangeTagDefs, false ) );
+       }
+
+       public function testRunUpdateHitCountMigrationWriteBoth() {
+               $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_WRITE_BOTH );
+               $changeTagDefBadRows = [
+                       [
+                               'ctd_name' => 'One Tag',
+                               'ctd_user_defined' => 0,
+                               'ctd_count' => 50,
+                       ],
+                       [
+                               'ctd_name' => 'Two Tags',
+                               'ctd_user_defined' => 0,
+                               'ctd_count' => 4,
+                       ],
+                       [
+                               'ctd_name' => 'Three Tags',
+                               'ctd_user_defined' => 0,
+                               'ctd_count' => 3,
+                       ],
+               ];
+               wfGetDB( DB_MASTER )->insert(
+                       'change_tag_def',
+                       $changeTagDefBadRows
+               );
+
+               $this->maintenance->loadWithArgv( [ '--sleep', '0' ] );
+
+               $this->maintenance->execute();
+
+               $changeTagDefRows = [
+                       (object)[
+                               'ctd_name' => 'One Tag',
+                               'ctd_count' => 1,
+                       ],
+                       (object)[
+                               'ctd_name' => 'Two Tags',
+                               'ctd_count' => 2,
+                       ],
+                       (object)[
+                               'ctd_name' => 'Three Tags',
+                               'ctd_count' => 3,
+                       ],
+               ];
+
+               $actualChangeTagDefs = wfGetDB( DB_REPLICA )->select(
+                       [ 'change_tag_def' ],
+                       [ 'ctd_name', 'ctd_count' ],
+                       [],
+                       __METHOD__,
+                       [ 'ORDER BY' => 'ctd_count' ]
+               );
+
+               $this->assertEquals( $changeTagDefRows, iterator_to_array( $actualChangeTagDefs, false ) );
+       }
+
+       public function testDryRunMigrationNew() {
+               $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_NEW );
+               $this->maintenance->loadWithArgv( [ '--dry-run', '--sleep', '0' ] );
+
+               $this->maintenance->execute();
+
+               $actualChangeTagDefs = wfGetDB( DB_REPLICA )->select(
+                       [ 'change_tag_def' ],
+                       [ 'ctd_id', 'ctd_name' ]
+               );
+
+               $this->assertEquals( [], iterator_to_array( $actualChangeTagDefs, false ) );
+
+               $actualChangeTags = wfGetDB( DB_REPLICA )->select(
+                       [ 'change_tag' ],
+                       [ 'ct_tag_id', 'ct_tag' ]
+               );
+
+               foreach ( $actualChangeTags as $row ) {
+                       $this->assertNull( $row->ct_tag_id );
+                       $this->assertNotNull( $row->ct_tag );
+               }
+       }
+
+       public function testDryRunMigrationWriteBoth() {
+               $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_WRITE_BOTH );
+               $this->maintenance->loadWithArgv( [ '--dry-run', '--sleep', '0' ] );
+
+               $this->maintenance->execute();
+
+               $actualChangeTagDefs = wfGetDB( DB_REPLICA )->select(
+                       [ 'change_tag_def' ],
+                       [ 'ctd_id', 'ctd_name' ]
+               );
+
+               $this->assertEquals( [], iterator_to_array( $actualChangeTagDefs, false ) );
+
+               $actualChangeTags = wfGetDB( DB_REPLICA )->select(
+                       [ 'change_tag' ],
+                       [ 'ct_tag_id', 'ct_tag' ]
+               );
+
+               foreach ( $actualChangeTags as $row ) {
+                       $this->assertNull( $row->ct_tag_id );
+                       $this->assertNotNull( $row->ct_tag );
+               }
+       }
+
+}
diff --git a/tests/phpunit/mocks/search/MockCompletionSearchEngine.php b/tests/phpunit/mocks/search/MockCompletionSearchEngine.php
new file mode 100644 (file)
index 0000000..ac8a5dc
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+
+/**
+ * SearchEngine implementation for returning mocked completion search results.
+ */
+class MockCompletionSearchEngine extends SearchEngine {
+       /** @var string[][] */
+       private static $results = [];
+
+       /**
+        * Reset any mocked results
+        */
+       public static function clearMockResults() {
+               self::$results = [];
+       }
+
+       /**
+        * Allows returning arbitrary lists of titles for completion search.
+        * Provided results will be sliced based on offset/limit of query.
+        *
+        * For results to exit the search engine they must pass Title::isKnown.
+        * Injecting into link cache is not enough, as LinkBatch will mark them
+        * bad, they need to be injected into the DB.
+        *
+        * @param string $query Search term as seen in completionSearchBackend
+        * @param string[] $result List of titles to respond to query with
+        */
+       public static function addMockResults( $query, array $result ) {
+               // Leading : ensures we don't treat another : as a namespace separator
+               $normalized = Title::newFromText( ":$query" )->getText();
+               self::$results[$normalized] = $result;
+       }
+
+       public function completionSearchBackend( $search ) {
+               if ( !isset( self::$results[$search] ) ) {
+                       return SearchSuggestionSet::emptySuggestionSet();
+               }
+               $results = array_slice( self::$results[$search], $this->offset, $this->limit );
+
+               return SearchSuggestionSet::fromStrings( $results );
+       }
+}
diff --git a/tests/phpunit/mocks/search/MockSearchEngine.php b/tests/phpunit/mocks/search/MockSearchEngine.php
new file mode 100644 (file)
index 0000000..2b7ea47
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+
+use MediaWiki\MediaWikiServices;
+
+class MockSearchEngine extends SearchEngine {
+       /** @var SearchResult[][] */
+       private static $results = [];
+       /** @var SearchResultSet[][] */
+       private static $interwikiResults = [];
+
+       public static function clearMockResults() {
+               self::$results = [];
+               self::$interwikiResults = [];
+       }
+
+       /**
+        * @param string $query The query searched for *after* initial
+        *  transformations have been applied.
+        * @param SearchResult[] $results The results to return for $query
+        */
+       public static function addMockResults( $query, array $results ) {
+               self::$results[$query] = $results;
+               $lc = MediaWikiServices::getInstance()->getLinkCache();
+               foreach ( $results as $result ) {
+                       // TODO: better page ids? Does it matter?
+                       $lc->addGoodLinkObj( mt_rand(), $result->getTitle() );
+               }
+       }
+
+       /**
+        * @param SearchResultSet[][] $interwikiResults
+        */
+       public static function setMockInterwikiResults( array $interwikiResults ) {
+               self::$interwikiResults = $interwikiResults;
+       }
+
+       protected function doSearchText( $term ) {
+               if ( isset( self::$results[ $term ] ) ) {
+                       $results = array_slice( self::$results[ $term ], $this->offset, $this->limit );
+               } else {
+                       $results = [];
+               }
+               return new MockSearchResultSet( $results, self::$interwikiResults );
+       }
+}
diff --git a/tests/phpunit/mocks/search/MockSearchResult.php b/tests/phpunit/mocks/search/MockSearchResult.php
new file mode 100644 (file)
index 0000000..d92d39a
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+class MockSearchResult extends SearchResult {
+       private $isMissingRevision = false;
+       private $isBrokenTitle = false;
+
+       public function isMissingRevision() {
+               return $this->isMissingRevision;
+       }
+       public function setMissingRevision( $isMissingRevision ) {
+               $this->isMissingRevision = $isMissingRevision;
+               return $this;
+       }
+
+       public function isBrokenTitle() {
+               return $this->isBrokenTitle;
+       }
+
+       public function setBrokenTitle( $isBrokenTitle ) {
+               $this->isBrokenTitle = $isBrokenTitle;
+               return $this;
+       }
+
+       public function getInterwikiPrefix() {
+               return $this->interwikiPrefix;
+       }
+
+       public function setInterwikiPrefix( $interwikiPrefix ) {
+               $this->interwikiPrefix = $interwikiPrefix;
+               return $this;
+       }
+}
diff --git a/tests/phpunit/mocks/search/MockSearchResultSet.php b/tests/phpunit/mocks/search/MockSearchResultSet.php
new file mode 100644 (file)
index 0000000..20e2a9f
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+
+class MockSearchResultSet extends SearchResultSet {
+       /*
+        * @var SearchResultSet[][] Map from result type to list of results for
+        *  that type.
+        */
+       private $interwikiResults;
+
+       /**
+        * @param SearchResult[] $results
+        * @param SearchResultSet[][] $interwikiResults Map from result type
+        *  to list of results for that type.
+        */
+       public function __construct( array $results, array $interwikiResults = [] ) {
+               parent::__construct( false, false );
+               $this->results = $results;
+               $this->interwikiResults = $interwikiResults;
+       }
+
+       public function numRows() {
+               return count( $this->results );
+       }
+
+       public function hasInterwikiResults( $type = self::SECONDARY_RESULTS ) {
+               return isset( $this->interwikiResults[$type] ) &&
+                       count( $this->interwikiResults[$type] ) > 0;
+       }
+
+       public function getInterwikiResults( $type = self::SECONDARY_RESULTS ) {
+               if ( $this->hasInterwikiResults( $type ) ) {
+                       return $this->interwikiResults[$type];
+               } else {
+                       return null;
+               }
+       }
+}
index 6c2ff02..ea132e9 100644 (file)
@@ -35,6 +35,9 @@ class AvailableRightsTest extends PHPUnit\Framework\TestCase {
                return $rights;
        }
 
+       /**
+        * @coversNothing
+        */
        public function testAvailableRights() {
                $missingRights = array_diff(
                        $this->getAllVisibleRights(),
index c8bcd60..c75a9d0 100644 (file)
@@ -32,6 +32,7 @@ class ContentHandlerSanityTest extends MediaWikiTestCase {
        }
 
        /**
+        * @coversNothing
         * @dataProvider provideHandlers
         * @param ContentHandler $handler
         */
index 60c97cc..dea8f5a 100644 (file)
@@ -19,6 +19,8 @@
 /**
  * Validates all loaded extensions and skins using the ExtensionRegistry
  * against the extension.json schema in the docs/ folder.
+ *
+ * @coversNothing
  */
 class ExtensionJsonValidationTest extends PHPUnit\Framework\TestCase {
 
index 62ddace..2090e29 100644 (file)
@@ -11,6 +11,7 @@
  * @copyright © 2012, Niklas Laxström
  * @copyright © 2012, Santhosh Thottingal
  * @copyright © 2012, Timo Tijhof
+ * @coversNothing
  */
 class ResourcesTest extends MediaWikiTestCase {
 
index 4df791e..82302b1 100644 (file)
@@ -9,6 +9,7 @@ class StructureTest extends MediaWikiTestCase {
         * Verify all files that appear to be tests have file names ending in
         * Test.  If the file names do not end in Test, they will not be run.
         * @group medium
+        * @coversNothing
         */
        public function testUnitTestFileNamesEndWithTest() {
                if ( wfIsWindows() ) {
index 02934fa..7d1d331 100644 (file)
@@ -45,6 +45,9 @@ class ExtensionsTestSuite extends PHPUnit_Framework_TestSuite {
  * when no extensions with tests are used.
  */
 class DummyExtensionsTest extends MediaWikiTestCase {
+       /**
+        * @coversNothing
+        */
        public function testNothing() {
                $this->assertTrue( true );
        }
diff --git a/tests/selenium/pageobjects/recentchanges.page.js b/tests/selenium/pageobjects/recentchanges.page.js
new file mode 100644 (file)
index 0000000..02d3843
--- /dev/null
@@ -0,0 +1,18 @@
+const Page = require( 'wdio-mediawiki/Page' );
+
+class RecentChangesPage extends Page {
+       get changesList() { return browser.element( '.mw-changeslist' ); }
+       get changesListTitles() { return this.changesList.$$( '.mw-changeslist-title' ); }
+       get titles() {
+               return this.changesListTitles.map( function ( element ) {
+                       return element.getText();
+               } );
+       }
+
+       open() {
+               super.openTitle( 'Special:RecentChanges' );
+       }
+
+}
+
+module.exports = new RecentChangesPage();
index 4a5c254..f6bb944 100755 (executable)
@@ -3,10 +3,13 @@ set -euo pipefail
 # Check the command before running in background so
 # that it can actually fail and have a descriptive error
 hash chromedriver
-chromedriver --url-base=/wd/hub --port=4444 &
+chromedriver --url-base=wd/hub --port=4444 &
+CHROME_DRIVER_PID=$!
+echo chromedriver running with PID $CHROME_DRIVER_PID
 # Make sure it is killed to prevent file descriptors leak
 function kill_chromedriver() {
-    killall chromedriver > /dev/null
+    # Use kill instead of killall to increase chances of this working on Windows
+    kill $CHROME_DRIVER_PID > /dev/null
 }
 trap kill_chromedriver EXIT
 npm run selenium-test
index dfc6fa1..069a6aa 100644 (file)
@@ -10,8 +10,8 @@ describe( 'Page', function () {
        var content,
                name;
 
-       function getTestString() {
-               return Math.random().toString() + '-öäü-♠♣♥♦';
+       function getTestString( suffix = 'defaultsuffix' ) {
+               return Math.random().toString() + '-Iñtërnâtiônàlizætiøn☃-' + suffix;
        }
 
        before( function () {
@@ -22,8 +22,8 @@ describe( 'Page', function () {
 
        beforeEach( function () {
                browser.deleteCookie();
-               content = getTestString();
-               name = getTestString();
+               content = getTestString( 'beforeEach-content' );
+               name = getTestString( 'beforeEach-name' );
        } );
 
        it( 'should be creatable', function () {
@@ -36,7 +36,7 @@ describe( 'Page', function () {
        } );
 
        it( 'should be re-creatable', function () {
-               let initialContent = getTestString();
+               let initialContent = getTestString( 'initialContent' );
 
                // create
                browser.call( function () {
@@ -63,11 +63,12 @@ describe( 'Page', function () {
                } );
 
                // edit
-               EditPage.edit( name, content );
+               let editContent = getTestString( 'editContent' );
+               EditPage.edit( name, editContent );
 
                // check
                assert.strictEqual( EditPage.heading.getText(), name );
-               assert.strictEqual( EditPage.displayedContent.getText(), content );
+               assert.strictEqual( EditPage.displayedContent.getText(), editContent );
        } );
 
        it( 'should have history', function () {
diff --git a/tests/selenium/specs/specialrecentchanges.js b/tests/selenium/specs/specialrecentchanges.js
new file mode 100644 (file)
index 0000000..418fbb1
--- /dev/null
@@ -0,0 +1,29 @@
+const assert = require( 'assert' ),
+       Api = require( 'wdio-mediawiki/Api' ),
+       RecentChangesPage = require( '../pageobjects/recentchanges.page' );
+
+describe( 'Special:RecentChanges', function () {
+       let content,
+               name;
+
+       function getTestString() {
+               return Math.random().toString() + '-öäü-♠♣♥♦';
+       }
+
+       beforeEach( function () {
+               browser.deleteCookie();
+               content = getTestString();
+               name = getTestString();
+       } );
+
+       it( 'shows page creation', function () {
+               browser.call( function () {
+                       return Api.edit( name, content );
+               } );
+
+               RecentChangesPage.open();
+
+               assert.strictEqual( name, RecentChangesPage.titles[ 0 ] );
+       } );
+
+} );