Merge "mediawiki.user: Small perf tweak for generateRandomSessionId"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Sat, 25 Aug 2018 05:17:58 +0000 (05:17 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Sat, 25 Aug 2018 05:17:58 +0000 (05:17 +0000)
44 files changed:
.phpcs.xml
RELEASE-NOTES-1.32
autoload.php
includes/DefaultSettings.php
includes/EditPage.php
includes/Storage/PageUpdater.php
includes/Storage/RevisionStore.php
includes/actions/McrUndoAction.php [new file with mode: 0644]
includes/api/i18n/en.json
includes/api/i18n/fr.json
includes/api/i18n/hu.json
includes/api/i18n/ko.json
includes/api/i18n/pt-br.json
includes/api/i18n/pt.json
includes/api/i18n/ru.json
includes/api/i18n/sv.json
includes/api/i18n/zh-hans.json
includes/api/i18n/zh-hant.json
includes/content/ContentHandler.php
includes/context/DerivativeContext.php
includes/db/CloneDatabase.php
includes/diff/DifferenceEngine.php
includes/pager/IndexPager.php
includes/skins/Skin.php
includes/specials/SpecialChangeCredentials.php
includes/specials/SpecialContributions.php
includes/specials/SpecialEmailuser.php
includes/specials/SpecialLinkAccounts.php
includes/specials/SpecialUnlinkAccounts.php
includes/specials/SpecialUpload.php
includes/specials/SpecialUploadStash.php
includes/specials/pagers/ImageListPager.php
languages/i18n/en.json
languages/i18n/hy.json
languages/i18n/ja.json
languages/i18n/my.json
languages/i18n/qqq.json
languages/i18n/ru.json
languages/i18n/tt-cyrl.json
maintenance/tables.sql
resources/src/startup/mediawiki.js
tests/phpunit/includes/Storage/McrReadNewRevisionStoreDbTest.php
tests/phpunit/includes/Storage/McrRevisionStoreDbTest.php
tests/phpunit/includes/diff/DifferenceEngineTest.php

index 10394d3..65ddb73 100644 (file)
@@ -16,7 +16,6 @@
                <exclude name="MediaWiki.Commenting.MissingCovers.MissingCovers" />
                <exclude name="MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName" />
                <exclude name="MediaWiki.Usage.DbrQueryUsage.DbrQueryFound" />
-               <exclude name="MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage" />
                <exclude name="MediaWiki.Usage.ForbiddenFunctions.passthru" />
                <exclude name="MediaWiki.VariableAnalysis.ForbiddenGlobalVariables.ForbiddenGlobal$wgTitle" />
                <exclude name="MediaWiki.WhiteSpace.SpaceBeforeSingleLineComment.NewLineComment" />
index a7fc232..ec8c022 100644 (file)
@@ -75,6 +75,11 @@ production.
   render diffs between two Content objects, and DifferenceEngine::setRevisions()
   to render diffs between two custom (potentially multi-content) revisions.
   Added GetSlotDiffRenderer hook which works like GetDifferenceEngine for slots.
+* Added a temporary action=mcrundo to the web UI, as the normal undo logic
+  can't yet handle MCR and deadlines are forcing is to put off fixing that.
+  This action should be considered deprecated and should not be used directly.
+* Extensions overriding ContentHandler::getUndoContent() will need to be
+  updated for the changed method signature.
 
 === External library changes in 1.32 ===
 * …
@@ -389,6 +394,8 @@ because of Phabricator reports.
   MediaWikiServices.
 * mw.user.stickyRandomId was renamed to the more explicit
   mw.user.getPageviewToken to better capture its function.
+* Passing Revision objects to ContentHandler::getUndoContent() is deprecated,
+  Content object should be passed instead.
 
 === Other changes in 1.32 ===
 * (T198811) The following tables have had their UNIQUE indexes turned into
index cef68b0..e6ae2bf 100644 (file)
@@ -851,6 +851,7 @@ $wgAutoloadLocalClasses = [
        'MappedIterator' => __DIR__ . '/includes/libs/MappedIterator.php',
        'MarkpatrolledAction' => __DIR__ . '/includes/actions/MarkpatrolledAction.php',
        'McTest' => __DIR__ . '/maintenance/mctest.php',
+       'McrUndoAction' => __DIR__ . '/includes/actions/McrUndoAction.php',
        'MediaHandler' => __DIR__ . '/includes/media/MediaHandler.php',
        'MediaHandlerFactory' => __DIR__ . '/includes/media/MediaHandlerFactory.php',
        'MediaStatisticsPage' => __DIR__ . '/includes/specials/SpecialMediaStatistics.php',
index 164eb46..9b0899d 100644 (file)
@@ -8008,6 +8008,7 @@ $wgActions = [
        'history' => true,
        'info' => true,
        'markpatrolled' => true,
+       'mcrundo' => McrUndoAction::class,
        'protect' => true,
        'purge' => true,
        'raw' => true,
index d1f874e..e087a6e 100644 (file)
@@ -684,7 +684,10 @@ class EditPage {
                # checking, etc.
                if ( 'initial' == $this->formtype || $this->firsttime ) {
                        if ( $this->initialiseForm() === false ) {
-                               $this->noSuchSectionPage();
+                               $out = $this->context->getOutput();
+                               if ( $out->getRedirect() === '' ) { // mcrundo hack redirects, don't override it
+                                       $this->noSuchSectionPage();
+                               }
                                return;
                        }
 
@@ -1220,8 +1223,13 @@ class EditPage {
                                                !$oldrev->isDeleted( Revision::DELETED_TEXT )
                                        ) {
                                                if ( WikiPage::hasDifferencesOutsideMainSlot( $undorev, $oldrev ) ) {
-                                                       // Cannot yet undo edits that involve anything other the main slot.
-                                                       $undoMsg = 'main-slot-only';
+                                                       // Hack for undo while EditPage can't handle multi-slot editing
+                                                       $this->context->getOutput()->redirect( $this->mTitle->getFullURL( [
+                                                               'action' => 'mcrundo',
+                                                               'undo' => $undo,
+                                                               'undoafter' => $undoafter,
+                                                       ] ) );
+                                                       return false;
                                                } else {
                                                        $content = $this->page->getUndoContent( $undorev, $oldrev );
 
index c6795ea..838efcd 100644 (file)
@@ -351,6 +351,15 @@ class PageUpdater {
                $this->slotsUpdate->modifyContent( $role, $content );
        }
 
+       /**
+        * Set the new slot for the given slot role
+        *
+        * @param SlotRecord $slot
+        */
+       public function setSlot( SlotRecord $slot ) {
+               $this->slotsUpdate->modifySlot( $slot );
+       }
+
        /**
         * Explicitly inherit a slot from some earlier revision.
         *
index 88d520c..d219267 100644 (file)
@@ -1547,6 +1547,10 @@ class RevisionStore
                $slots = [];
 
                foreach ( $res as $row ) {
+                       // resolve role names and model names from in-memory cache, instead of joining.
+                       $row->role_name = $this->slotRoleStore->getName( (int)$row->slot_role_id );
+                       $row->model_name = $this->contentModelStore->getName( (int)$row->content_model );
+
                        $contentCallback = function ( SlotRecord $slot ) use ( $queryFlags, $row ) {
                                return $this->loadSlotContent( $slot, null, null, null, $queryFlags );
                        };
@@ -2268,6 +2272,9 @@ class RevisionStore
         *
         * @param array $options Any combination of the following strings
         *  - 'content': Join with the content table, and select content meta-data fields
+        *  - 'model': Join with the content_models table, and select the model_name field.
+        *             Only applicable if 'content' is also set.
+        *  - 'role': Join with the slot_roles table, and select the role_name field
         *
         * @return array With three keys:
         *  - tables: (string[]) to include in the `$table` to `IDatabase->select()`
@@ -2304,26 +2311,39 @@ class RevisionStore
                        }
                } else {
                        $ret['tables'][] = 'slots';
-                       $ret['tables'][] = 'slot_roles';
                        $ret['fields'] = array_merge( $ret['fields'], [
                                'slot_revision_id',
                                'slot_content_id',
                                'slot_origin',
-                               'role_name'
+                               'slot_role_id',
                        ] );
-                       $ret['joins']['slot_roles'] = [ 'INNER JOIN', [ 'slot_role_id = role_id' ] ];
+
+                       if ( in_array( 'role', $options, true ) ) {
+                               // Use left join to attach role name, so we still find the revision row even
+                               // if the role name is missing. This triggers a more obvious failure mode.
+                               $ret['tables'][] = 'slot_roles';
+                               $ret['joins']['slot_roles'] = [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ];
+                               $ret['fields'][] = 'role_name';
+                       }
 
                        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'
+                                       'content_model',
                                ] );
                                $ret['joins']['content'] = [ 'INNER JOIN', [ 'slot_content_id = content_id' ] ];
-                               $ret['joins']['content_models'] = [ 'INNER JOIN', [ 'content_model = model_id' ] ];
+
+                               if ( in_array( 'model', $options, true ) ) {
+                                       // Use left join to attach model name, so we still find the revision row even
+                                       // if the model name is missing. This triggers a more obvious failure mode.
+                                       $ret['tables'][] = 'content_models';
+                                       $ret['joins']['content_models'] = [ 'LEFT JOIN', [ 'content_model = model_id' ] ];
+                                       $ret['fields'][] = 'model_name';
+                               }
+
                        }
                }
 
diff --git a/includes/actions/McrUndoAction.php b/includes/actions/McrUndoAction.php
new file mode 100644 (file)
index 0000000..90d1f68
--- /dev/null
@@ -0,0 +1,376 @@
+<?php
+/**
+ * Temporary action for MCR undos
+ * @file
+ * @ingroup Actions
+ */
+
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Storage\MutableRevisionRecord;
+use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Storage\SlotRecord;
+
+/**
+ * Temporary action for MCR undos
+ *
+ * This is intended to go away when real MCR support is added to EditPage and
+ * the standard undo-with-edit behavior can be implemented there instead.
+ *
+ * If this were going to be kept, we'd probably want to figure out a good way
+ * to reuse the same code for generating the headers, summary box, and buttons
+ * on EditPage and here, and to better share the diffing and preview logic
+ * between the two. But doing that now would require much of the rewriting of
+ * EditPage that we're trying to put off by doing this instead.
+ *
+ * @ingroup Actions
+ * @since 1.32
+ * @deprecated since 1.32
+ */
+class McrUndoAction extends FormAction {
+
+       private $undo = 0, $undoafter = 0, $cur = 0;
+
+       /** @param RevisionRecord|null */
+       private $curRev = null;
+
+       public function getName() {
+               return 'mcrundo';
+       }
+
+       public function getDescription() {
+               return '';
+       }
+
+       public function show() {
+               // Send a cookie so anons get talk message notifications
+               // (copied from SubmitAction)
+               MediaWiki\Session\SessionManager::getGlobalSession()->persist();
+
+               // Some stuff copied from EditAction
+               $this->useTransactionalTimeLimit();
+
+               $out = $this->getOutput();
+               $out->setRobotPolicy( 'noindex,nofollow' );
+               if ( $this->getContext()->getConfig()->get( 'UseMediaWikiUIEverywhere' ) ) {
+                       $out->addModuleStyles( [
+                               'mediawiki.ui.input',
+                               'mediawiki.ui.checkbox',
+                       ] );
+               }
+
+               // IP warning headers copied from EditPage
+               // (should more be copied?)
+               if ( wfReadOnly() ) {
+                       $out->wrapWikiMsg(
+                               "<div id=\"mw-read-only-warning\">\n$1\n</div>",
+                               [ 'readonlywarning', wfReadOnlyReason() ]
+                       );
+               } elseif ( $this->context->getUser()->isAnon() ) {
+                       if ( !$this->getRequest()->getCheck( 'wpPreview' ) ) {
+                               $out->wrapWikiMsg(
+                                       "<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
+                                       [ 'anoneditwarning',
+                                               // Log-in link
+                                               SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [
+                                                       'returnto' => $this->getTitle()->getPrefixedDBkey()
+                                               ] ),
+                                               // Sign-up link
+                                               SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [
+                                                       'returnto' => $this->getTitle()->getPrefixedDBkey()
+                                               ] )
+                                       ]
+                               );
+                       } else {
+                               $out->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
+                                       'anonpreviewwarning'
+                               );
+                       }
+               }
+
+               parent::show();
+       }
+
+       protected function checkCanExecute( User $user ) {
+               parent::checkCanExecute( $user );
+
+               $this->undoafter = $this->getRequest()->getInt( 'undoafter' );
+               $this->undo = $this->getRequest()->getInt( 'undo' );
+
+               if ( $this->undo == 0 || $this->undoafter == 0 ) {
+                       throw new ErrorPageError( 'mcrundofailed', 'mcrundo-missingparam' );
+               }
+
+               $curRev = $this->page->getRevision();
+               if ( !$curRev ) {
+                       throw new ErrorPageError( 'mcrundofailed', 'nopagetext' );
+               }
+               $this->curRev = $curRev->getRevisionRecord();
+               $this->cur = $this->getRequest()->getInt( 'cur', $this->curRev->getId() );
+
+               $revisionLookup = MediaWikiServices::getInstance()->getRevisionLookup();
+
+               $undoRev = $revisionLookup->getRevisionById( $this->undo );
+               $oldRev = $revisionLookup->getRevisionById( $this->undoafter );
+
+               if ( $undoRev === null || $oldRev === null ||
+                       $undoRev->isDeleted( RevisionRecord::DELETED_TEXT ) ||
+                       $oldRev->isDeleted( RevisionRecord::DELETED_TEXT )
+               ) {
+                       throw new ErrorPageError( 'mcrundofailed', 'undo-norev' );
+               }
+
+               return true;
+       }
+
+       /**
+        * @return MutableRevisionRecord
+        */
+       private function getNewRevision() {
+               $revisionLookup = MediaWikiServices::getInstance()->getRevisionLookup();
+
+               $undoRev = $revisionLookup->getRevisionById( $this->undo );
+               $oldRev = $revisionLookup->getRevisionById( $this->undoafter );
+               $curRev = $this->curRev;
+
+               $isLatest = $curRev->getId() === $undoRev->getId();
+
+               if ( $undoRev === null || $oldRev === null ||
+                       $undoRev->isDeleted( RevisionRecord::DELETED_TEXT ) ||
+                       $oldRev->isDeleted( RevisionRecord::DELETED_TEXT )
+               ) {
+                       throw new ErrorPageError( 'mcrundofailed', 'undo-norev' );
+               }
+
+               if ( $isLatest ) {
+                       // Short cut! Undoing the current revision means we just restore the old.
+                       return MutableRevisionRecord::newFromParentRevision( $oldRev );
+               }
+
+               $newRev = MutableRevisionRecord::newFromParentRevision( $curRev );
+
+               // Figure out the roles that need merging by first collecting all roles
+               // and then removing the ones that don't.
+               $rolesToMerge = array_unique( array_merge(
+                       $oldRev->getSlotRoles(),
+                       $undoRev->getSlotRoles(),
+                       $curRev->getSlotRoles()
+               ) );
+
+               // Any roles with the same content in $oldRev and $undoRev can be
+               // inherited because undo won't change them.
+               $rolesToMerge = array_intersect(
+                       $rolesToMerge, $oldRev->getSlots()->getRolesWithDifferentContent( $undoRev->getSlots() )
+               );
+               if ( !$rolesToMerge ) {
+                       throw new ErrorPageError( 'mcrundofailed', 'undo-nochange' );
+               }
+
+               // Any roles with the same content in $oldRev and $curRev were already reverted
+               // and so can be inherited.
+               $rolesToMerge = array_intersect(
+                       $rolesToMerge, $oldRev->getSlots()->getRolesWithDifferentContent( $curRev->getSlots() )
+               );
+               if ( !$rolesToMerge ) {
+                       throw new ErrorPageError( 'mcrundofailed', 'undo-nochange' );
+               }
+
+               // Any roles with the same content in $undoRev and $curRev weren't
+               // changed since and so can be reverted to $oldRev.
+               $diffRoles = array_intersect(
+                       $rolesToMerge, $undoRev->getSlots()->getRolesWithDifferentContent( $curRev->getSlots() )
+               );
+               foreach ( array_diff( $rolesToMerge, $diffRoles ) as $role ) {
+                       if ( $oldRev->hasSlot( $role ) ) {
+                               $newRev->inheritSlot( $oldRev->getSlot( $role, RevisionRecord::RAW ) );
+                       } else {
+                               $newRev->removeSlot( $role );
+                       }
+               }
+               $rolesToMerge = $diffRoles;
+
+               // Any slot additions or removals not handled by the above checks can't be undone.
+               // There will be only one of the three revisions missing the slot:
+               //  - !old means it was added in the undone revisions and modified after.
+               //    Should it be removed entirely for the undo, or should the modified version be kept?
+               //  - !undo means it was removed in the undone revisions and then readded with different content.
+               //    Which content is should be kept, the old or the new?
+               //  - !cur means it was changed in the undone revisions and then deleted after.
+               //    Did someone delete vandalized content instead of undoing (meaning we should ideally restore
+               //    it), or should it stay gone?
+               foreach ( $rolesToMerge as $role ) {
+                       if ( !$oldRev->hasSlot( $role ) || !$undoRev->hasSlot( $role ) || !$curRev->hasSlot( $role ) ) {
+                               throw new ErrorPageError( 'mcrundofailed', 'undo-failure' );
+                       }
+               }
+
+               // Try to merge anything that's left.
+               foreach ( $rolesToMerge as $role ) {
+                       $oldContent = $oldRev->getSlot( $role, RevisionRecord::RAW )->getContent();
+                       $undoContent = $undoRev->getSlot( $role, RevisionRecord::RAW )->getContent();
+                       $curContent = $curRev->getSlot( $role, RevisionRecord::RAW )->getContent();
+                       $newContent = $undoContent->getContentHandler()
+                               ->getUndoContent( $curContent, $undoContent, $oldContent, $isLatest );
+                       if ( !$newContent ) {
+                               throw new ErrorPageError( 'mcrundofailed', 'undo-failure' );
+                       }
+                       $newRev->setSlot( SlotRecord::newUnsaved( $role, $newContent ) );
+               }
+
+               return $newRev;
+       }
+
+       private function generateDiff() {
+               $newRev = $this->getNewRevision();
+               if ( $newRev->hasSameContent( $this->curRev ) ) {
+                       throw new ErrorPageError( 'mcrundofailed', 'undo-nochange' );
+               }
+
+               $diffEngine = new DifferenceEngine( $this->context );
+               $diffEngine->setRevisions( $this->curRev, $newRev );
+
+               $oldtitle = $this->context->msg( 'currentrev' )->parse();
+               $newtitle = $this->context->msg( 'yourtext' )->parse();
+
+               if ( $this->getRequest()->getCheck( 'wpPreview' ) ) {
+                       $diffEngine->renderNewRevision();
+                       return '';
+               } else {
+                       $diffText = $diffEngine->getDiff( $oldtitle, $newtitle );
+                       $diffEngine->showDiffStyle();
+                       return '<div id="wikiDiff">' . $diffText . '</div>';
+               }
+       }
+
+       public function onSubmit( $data ) {
+               global $wgUseRCPatrol;
+
+               if ( !$this->getRequest()->getCheck( 'wpSave' ) ) {
+                       // Diff or preview
+                       return false;
+               }
+
+               $updater = $this->page->getPage()->newPageUpdater( $this->context->getUser() );
+               $curRev = $updater->grabParentRevision();
+               if ( !$curRev ) {
+                       throw new ErrorPageError( 'mcrundofailed', 'nopagetext' );
+               }
+
+               if ( $this->cur !== $curRev->getId() ) {
+                       return Status::newFatal( 'mcrundo-changed' );
+               }
+
+               $newRev = $this->getNewRevision();
+               if ( !$newRev->hasSameContent( $curRev ) ) {
+                       // Copy new slots into the PageUpdater, and remove any removed slots.
+                       // TODO: This interface is awful, there should be a way to just pass $newRev.
+                       // TODO: MCR: test this once we can store multiple slots
+                       foreach ( $newRev->getSlots()->getSlots() as $slot ) {
+                               $updater->setSlot( $slot );
+                       }
+                       foreach ( $curRev->getSlotRoles() as $role ) {
+                               if ( !$newRev->hasSlot( $role ) ) {
+                                       $updater->removeSlot( $role );
+                               }
+                       }
+
+                       $updater->setOriginalRevisionId( false );
+                       $updater->setUndidRevisionId( $this->undo );
+
+                       // TODO: Ugh.
+                       if ( $wgUseRCPatrol && $this->getTitle()->userCan( 'autopatrol', $this->getUser() ) ) {
+                               $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
+                       }
+
+                       $updater->saveRevision(
+                               CommentStoreComment::newUnsavedComment( trim( $this->getRequest()->getVal( 'wpSummary' ) ) ),
+                               EDIT_AUTOSUMMARY | EDIT_UPDATE
+                       );
+
+                       return $updater->getStatus();
+               }
+
+               return Status::newGood();
+       }
+
+       protected function usesOOUI() {
+               return true;
+       }
+
+       protected function getFormFields() {
+               $request = $this->getRequest();
+               $config = $this->context->getConfig();
+               $oldCommentSchema = $config->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
+               $ret = [
+                       'diff' => [
+                               'type' => 'info',
+                               'vertical-label' => true,
+                               'raw' => true,
+                               'default' => function () {
+                                       return $this->generateDiff();
+                               }
+                       ],
+                       'summary' => [
+                               'type' => 'text',
+                               'id' => 'wpSummary',
+                               'name' => 'wpSummary',
+                               'cssclass' => 'mw-summary',
+                               'label-message' => 'summary',
+                               'maxlength' => $oldCommentSchema ? 200 : CommentStore::COMMENT_CHARACTER_LIMIT,
+                               'value' => $request->getVal( 'wpSummary', '' ),
+                               'size' => 60,
+                               'spellcheck' => 'true',
+                       ],
+                       'summarypreview' => [
+                               'type' => 'info',
+                               'label-message' => 'summary-preview',
+                               'raw' => true,
+                       ],
+               ];
+
+               if ( $request->getCheck( 'wpSummary' ) ) {
+                       $ret['summarypreview']['default'] = Xml::tags( 'div', [ 'class' => 'mw-summary-preview' ],
+                               Linker::commentBlock( trim( $request->getVal( 'wpSummary' ) ), $this->getTitle(), false )
+                       );
+               } else {
+                       unset( $ret['summarypreview'] );
+               }
+
+               return $ret;
+       }
+
+       protected function alterForm( HTMLForm $form ) {
+               $form->setWrapperLegendMsg( 'confirm-mcrundo-title' );
+
+               $labelAsPublish = $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' );
+
+               $form->setSubmitName( 'wpSave' );
+               $form->setSubmitTooltip( $labelAsPublish ? 'publish' : 'save' );
+               $form->setSubmitTextMsg( $labelAsPublish ? 'publishchanges' : 'savechanges' );
+               $form->showCancel( true );
+               $form->setCancelTarget( $this->getTitle() );
+               $form->addButton( [
+                       'name' => 'wpPreview',
+                       'value' => '1',
+                       'label-message' => 'showpreview',
+                       'attribs' => Linker::tooltipAndAccesskeyAttribs( 'preview' ),
+               ] );
+               $form->addButton( [
+                       'name' => 'wpDiff',
+                       'value' => '1',
+                       'label-message' => 'showdiff',
+                       'attribs' => Linker::tooltipAndAccesskeyAttribs( 'diff' ),
+               ] );
+
+               $form->addHiddenField( 'undo', $this->undo );
+               $form->addHiddenField( 'undoafter', $this->undoafter );
+               $form->addHiddenField( 'cur', $this->curRev->getId() );
+       }
+
+       public function onSuccess() {
+               $this->getOutput()->redirect( $this->getTitle()->getFullURL() );
+       }
+
+       protected function preText() {
+               return '<div style="clear:both"></div>';
+       }
+}
index e29ddf9..ae2ffd3 100644 (file)
@@ -82,8 +82,6 @@
        "apihelp-compare-param-toslots": "Override content of the revision specified by <var>totitle</var>, <var>toid</var> or <var>torev</var>.\n\nThis parameter specifies the slots that are to be modified. Use <var>totext-&#x7B;slot}</var>, <var>tocontentmodel-&#x7B;slot}</var>, and <var>tocontentformat-&#x7B;slot}</var> to specify content for each slot.",
        "apihelp-compare-param-totext-{slot}": "Text of the specified slot. If omitted, the slot is removed from the revision.",
        "apihelp-compare-param-tosection-{slot}": "When <var>totext-&#x7B;slot}</var> is the content of a single section, this is the section number. It will be merged into the revision specified by <var>totitle</var>, <var>toid</var> or <var>torev</var> as if for a section edit.",
-       "apihelp-compare-param-toslots": "Specify content to use instead of the content of the revision specified by <var>totitle</var>, <var>toid</var> or <var>torev</var>.\n\nThis parameter specifies the slots that have content. Use <var>totext-&#x7B;slot}</var>, <var>tocontentmodel-&#x7B;slot}</var>, and <var>tocontentformat-&#x7B;slot}</var> to specify content for each slot.",
-       "apihelp-compare-param-totext-{slot}": "Text of the specified slot.",
        "apihelp-compare-param-tocontentmodel-{slot}": "Content model of <var>totext-&#x7B;slot}</var>. If not supplied, it will be guessed based on the other parameters.",
        "apihelp-compare-param-tocontentformat-{slot}": "Content serialization format of <var>totext-&#x7B;slot}</var>.",
        "apihelp-compare-param-totext": "Specify <kbd>toslots=main</kbd> and use <var>totext-main</var> instead.",
index e8d4aac..ed3a6ab 100644 (file)
        "apihelp-compare-param-fromtitle": "Premier titre à comparer.",
        "apihelp-compare-param-fromid": "ID de la première page à comparer.",
        "apihelp-compare-param-fromrev": "Première révision à comparer.",
-       "apihelp-compare-param-frompst": "Faire une transformation avant enregistrement sur <var>fromtext</var>.",
-       "apihelp-compare-param-fromtext": "Utiliser ce texte au lieu du contenu de la révision spécifié par <var>fromtitle</var>, <var>fromid</var> ou <var>fromrev</var>.",
-       "apihelp-compare-param-fromcontentmodel": "Modèle de contenu de <var>fromtext</var>. Si non fourni, il sera déduit d’après les autres paramètres.",
-       "apihelp-compare-param-fromcontentformat": "Sérialisation du contenu de <var>fromtext</var>.",
+       "apihelp-compare-param-frompst": "Faire une transformation avant enregistrement sur <var>fromtext-&#x7B;slot}</var>.",
+       "apihelp-compare-param-fromslots": "Substituer le contenu de la révision spécifiée par <var>fromtitle</var>, <var>fromid</var> ou <var>fromrev</var>.\n\nCe paramètre spécifie les intervalles à modifier. Utilisez <var>fromtext-&#x7B;slot}</var>, <var>fromcontentmodel-&#x7B;slot}</var>, et <var>fromcontentformat-&#x7B;slot}</var> pour spécifier le contenu de chaque intervalle.",
+       "apihelp-compare-param-fromtext-{slot}": "Texte de l'intervalle spécifié. Si absent, l'intervalle est supprimé de la révision.",
+       "apihelp-compare-param-fromsection-{slot}": "Si <var>fromtext-&#x7B;slot}</var> est le contenu d'une seule section, c'est le numéro de la section. Il sera fusionné dans la révision spécifiée par <var>fromtitle</var>, <var>fromid</var> ou <var>fromrev</var> comme pour les modifications de section.",
+       "apihelp-compare-param-fromcontentmodel-{slot}": "Modèle de contenu de <var>fromtext-&#x7B;slot}</var>. Si non fourni, il sera déduit en fonction de la valeur des autres paramètres.",
+       "apihelp-compare-param-fromcontentformat-{slot}": "Format de sérialisation de contenu de <var>fromtext-&#x7B;slot}</var>.",
+       "apihelp-compare-param-fromtext": "Spécifiez <kbd>fromslots=main</kbd> et utilisez <var>fromtext-main</var> à la place.",
+       "apihelp-compare-param-fromcontentmodel": "Spécifiez <kbd>fromslots=main</kbd> et utilisez <var>fromcontentmodel-main</var> à la place.",
+       "apihelp-compare-param-fromcontentformat": "Spécifiez <kbd>fromslots=main</kbd> et utilisez <var>fromcontentformat-main</var> à la place.",
        "apihelp-compare-param-fromsection": "N'utiliser que la section spécifiée du contenu 'from'.",
        "apihelp-compare-param-totitle": "Second titre à comparer.",
        "apihelp-compare-param-toid": "ID de la seconde page à comparer.",
        "apihelp-compare-param-torev": "Seconde révision à comparer.",
        "apihelp-compare-param-torelative": "Utiliser une révision relative à la révision déterminée de <var>fromtitle</var>, <var>fromid</var> ou <var>fromrev</var>. Toutes les autres options 'to' seront ignorées.",
        "apihelp-compare-param-topst": "Faire une transformation avant enregistrement sur <var>totext</var>.",
-       "apihelp-compare-param-totext": "Utiliser ce texte au lieu du contenu de la révision spécifié par <var>totitle</var>, <var>toid</var> ou <var>torev</var>.",
-       "apihelp-compare-param-tocontentmodel": "Modèle de contenu de <var>totext</var>. Si non fourni, il sera deviné d’après les autres paramètres.",
-       "apihelp-compare-param-tocontentformat": "Format de sérialisation du contenu de <var>totext</var>.",
+       "apihelp-compare-param-toslots": "Spécifiez le contenu à utiliser au lieu du contenu de la révision spécifiée par <var>totitle</var>, <var>toid</var> ou <var>torev</var>.\n\nCe paramètre spécifie les intervalles qui ont du contenu. Utilisez <var>totext-&#x7B;slot}</var>, <var>tocontentmodel-&#x7B;slot}</var>, et <var>tocontentformat-&#x7B;slot}</var> pour spécifier le contenue de chaque intervalle.",
+       "apihelp-compare-param-totext-{slot}": "Texte de l'intervalle spécifique.",
+       "apihelp-compare-param-tosection-{slot}": "Si <var>totext-&#x7B;slot}</var> est le contenu d'une seule section, c'est le numéro de la section. Il sera fusionné dans la révision spécifiée par <var>totitle</var>, <var>toid</var> ou <var>torev</var> comme pour les modifications de section.",
+       "apihelp-compare-param-tocontentmodel-{slot}": "Modèle de contenu de <var>totext-&#x7B;slot}</var>. Si non fourni, il sera déduit en fonction de la valeur des autres paramètres.",
+       "apihelp-compare-param-tocontentformat-{slot}": "Format de sérialisation du contenu de <var>totext-&#x7B;slot}</var>.",
+       "apihelp-compare-param-totext": "Spécifiez <kbd>toslots=main</kbd> et utilisez <var>totext-main</var> à la place.",
+       "apihelp-compare-param-tocontentmodel": "Spécifiez <kbd>toslots=main</kbd> et utilisez <var>tocontentmodel-main</var> à la place.",
+       "apihelp-compare-param-tocontentformat": "Spécifiez <kbd>toslots=main</kbd> et utilisez <var>tocontentformat-main</var> à la place.",
        "apihelp-compare-param-tosection": "N'utiliser que la section spécifiée du contenu 'to'.",
        "apihelp-compare-param-prop": "Quelles informations obtenir.",
        "apihelp-compare-paramvalue-prop-diff": "Le diff HTML.",
        "apihelp-compare-paramvalue-prop-comment": "Le commentaire des révisions 'depuis' et 'vers'.",
        "apihelp-compare-paramvalue-prop-parsedcomment": "Le commentaire analysé des révisions 'depuis' et 'vers'.",
        "apihelp-compare-paramvalue-prop-size": "La taille des révisions 'depuis' et 'vers'.",
+       "apihelp-compare-param-slots": "Retourne les diffs individuels pour ces intervalles, plutôt qu'un diff combiné pour tous les intervalles.",
        "apihelp-compare-example-1": "Créer une différence entre les révisions 1 et 2",
        "apihelp-createaccount-summary": "Créer un nouveau compte utilisateur.",
        "apihelp-createaccount-param-preservestate": "Si <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> a retourné true pour <samp>hasprimarypreservedstate</samp>, les demandes marquées comme <samp>primary-required</samp> doivent être omises. Si elle a retourné une valeur non vide pour <samp>preservedusername</samp>, ce nom d'utilisateur doit être utilisé pour le paramètre <var>username</var>.",
        "apierror-compare-no-title": "Impossible de faire une transformation avant enregistrement sans titre. Essayez de spécifier <var>fromtitle</var> ou <var>totitle</var>.",
        "apierror-compare-nosuchfromsection": "Il n'y a pas de section $1 dans le contenu 'from'.",
        "apierror-compare-nosuchtosection": "Il n'y a pas de section $1 dans le contenu 'to'.",
+       "apierror-compare-nofromrevision": "Aucune révision 'from'. Spécifiez <var>fromrev</var>, <var>fromtitle</var>, ou <var>fromid</var>.",
+       "apierror-compare-notorevision": "Aucune révision 'to'. Spécifiez <var>torev</var>, <var>totitle</var>, ou <var>toid</var>.",
        "apierror-compare-relative-to-nothing": "Pas de révision 'depuis' pour <var>torelative</var> à laquelle se rapporter.",
        "apierror-contentserializationexception": "Échec de sérialisation du contenu : $1",
        "apierror-contenttoobig": "Le contenu que vous avez fourni dépasse la limite de taille d’un article, qui est de $1 {{PLURAL:$1|kilooctet|kilooctets}}.",
        "apierror-mimesearchdisabled": "La recherche MIME est désactivée en mode Misère.",
        "apierror-missingcontent-pageid": "Contenu manquant pour la page d’ID $1.",
        "apierror-missingcontent-revid": "Contenu de la révision d’ID $1 manquant.",
+       "apierror-missingcontent-revid-role": "Contenu absent pour l'ID de révision $1 pour le rôle $2.",
        "apierror-missingparam-at-least-one-of": "{{PLURAL:$2|Le paramètre|Au moins un des paramètres}} $1 est obligatoire.",
        "apierror-missingparam-one-of": "{{PLURAL:$2|Le paramètre|Un des paramètres}} $1 est obligatoire.",
        "apierror-missingparam": "Le paramètre <var>$1</var> doit être défini.",
index 4451f19..1211693 100644 (file)
@@ -10,7 +10,7 @@
                        "Dj"
                ]
        },
-       "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Dokumentáció]]\n* [[mw:Special:MyLanguage/API:FAQ|GYIK]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Levelezőlista]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API-bejelentések]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Hibabejelentések és kérések]\n</div>\n<strong>Státusz:</strong> Minden ezen a lapon látható funkciónak működnie kell, de az API jelenleg is aktív fejlesztés alatt áll, és bármikor változhat. Iratkozz fel a [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ mediawiki-api-announce levelezőlistára] a frissítések követéséhez.\n\n<strong>Hibás kérések:</strong> Ha az API hibás kérést kap, egy HTTP-fejlécet küld vissza „MediaWiki-API-Error” kulccsal, és a fejléc értéke és a visszaküldött hibakód ugyanarra az értékre lesz állítva. További információért lásd: [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Hibák és figyelmeztetések]].\n\n<p class=\"mw-apisandbox-link\"><strong>Tesztelés:</strong> Az API-kérések könnyebb teszteléséhez használható az [[Special:ApiSandbox|API-homokozó]].</p>",
+       "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Dokumentáció]]\n* [[mw:Special:MyLanguage/API:FAQ|GYIK]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Levelezőlista]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API-bejelentések]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Hibabejelentések és kérések]\n</div>\n<strong>Állapot:</strong> A MediaWiki API egy érett és stabil interfész, ami aktív támogatásban és fejlesztésben részesül. Bár próbáljuk elkerülni, de néha szükség van visszafelé nem kompatibilis változtatásokra; iratkozz fel a [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ mediawiki-api-announce levelezőlistára] a frissítések követéséhez.\n\n<strong>Hibás kérések:</strong> Ha az API hibás kérést kap, egy HTTP-fejlécet küld vissza „MediaWiki-API-Error” kulccsal, és a fejléc értéke és a visszaküldött hibakód ugyanarra az értékre lesz állítva. További információért lásd: [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Hibák és figyelmeztetések]].\n\n<p class=\"mw-apisandbox-link\"><strong>Tesztelés:</strong> Az API-kérések könnyebb teszteléséhez használható az [[Special:ApiSandbox|API-homokozó]].</p>",
        "apihelp-main-param-action": "Milyen műveletet hajtson végre.",
        "apihelp-main-param-format": "A kimenet formátuma.",
        "apihelp-main-param-smaxage": "Az <code>s-maxage</code> gyorsítótár-vezérlő HTTP-fejléc beállítása ennyi másodpercre. A hibák soha nincsenek gyorsítótárazva.",
index 247c928..f571697 100644 (file)
        "apihelp-compare-param-fromtitle": "비교할 첫 이름.",
        "apihelp-compare-param-fromid": "비교할 첫 문서 ID.",
        "apihelp-compare-param-fromrev": "비교할 첫 판.",
-       "apihelp-compare-param-frompst": "<var>fromtext</var>에 사전 저장 변환을 수행합니다.",
-       "apihelp-compare-param-fromtext": "<var>fromtitle</var>, <var>fromid</var> 또는 <var>fromrev</var>로 지정된 판의 내용 대신 이 텍스트를 사용합니다.",
-       "apihelp-compare-param-fromcontentmodel": "<var>fromtext</var>의 콘텐츠 모델입니다. 지정하지 않으면 다른 변수를 참고하여 추정합니다.",
-       "apihelp-compare-param-fromcontentformat": "<var>fromtext</var>의 콘텐츠 직렬화 포맷입니다.",
+       "apihelp-compare-param-frompst": "<var>fromtext-&#x7B;slot}</var>에 사전 저장 변환을 수행합니다.",
+       "apihelp-compare-param-fromtext-{slot}": "지정된 슬롯의 텍스트입니다. 생략할 경우 판에서 슬롯이 제거됩니다.",
+       "apihelp-compare-param-fromcontentmodel-{slot}": "<var>fromtext-&#x7B;slot}</var>의 콘텐츠 모델입니다. 지정하지 않으면 다른 변수를 참고하여 추정합니다.",
+       "apihelp-compare-param-fromcontentformat-{slot}": "<var>fromtext-&#x7B;slot}</var>의 콘텐츠 직렬화 포맷입니다.",
+       "apihelp-compare-param-fromtext": "<kbd>fromslots=main</kbd>을 지정하고 <var>fromtext-main</var>을 대신 사용합니다.",
+       "apihelp-compare-param-fromcontentmodel": "<kbd>fromslots=main</kbd>을 지정하고 <var>fromcontentmodel-main</var>을 대신 사용합니다.",
+       "apihelp-compare-param-fromcontentformat": "<kbd>fromslots=main</kbd>을 지정하고 <var>fromcontentformat-main</var>을 대신 사용합니다.",
        "apihelp-compare-param-fromsection": "지정된 'from' 내용의 지정된 문단만 사용합니다.",
        "apihelp-compare-param-totitle": "비교할 두 번째 제목.",
        "apihelp-compare-param-toid": "비교할 두 번째 문서 ID.",
        "apihelp-compare-param-torev": "비교할 두 번째 판.",
        "apihelp-compare-param-torelative": "<var>fromtitle</var>, <var>fromid</var> 또는 <var>fromrev</var>에서 결정된 판과 상대적인 판을 사용합니다. 다른 'to' 옵션들은 모두 무시됩니다.",
        "apihelp-compare-param-topst": "<var>totext</var>에 사전 저장 변환을 수행합니다.",
-       "apihelp-compare-param-totext": "<var>totitle</var>, <var>toid</var> 또는 <var>torev</var>로 지정된 판의 내용 대신 이 텍스트를 사용합니다.",
-       "apihelp-compare-param-tocontentmodel": "<var>totext</var>의 콘텐츠 모델입니다. 지정하지 않으면 다른 변수를 참고하여 추정합니다.",
-       "apihelp-compare-param-tocontentformat": "<var>totext</var>의 콘텐츠 직렬화 포맷입니다.",
+       "apihelp-compare-param-totext-{slot}": "지정된 슬롯의 텍스트입니다.",
+       "apihelp-compare-param-tocontentmodel-{slot}": "<var>totext-&#x7B;slot}</var>의 콘텐츠 모델입니다. 지정하지 않으면 다른 변수를 참고하여 추정합니다.",
+       "apihelp-compare-param-tocontentformat-{slot}": "<var>totext-&#x7B;slot}</var>의 콘텐츠 직렬화 포맷입니다.",
+       "apihelp-compare-param-totext": "<kbd>toslots=main</kbd>을 지정하고 <var>totext-main</var>을 대신 사용합니다.",
+       "apihelp-compare-param-tocontentmodel": "<kbd>toslots=main</kbd>을 지정하고 <var>tocontentmodel-main</var>을 대신 사용합니다.",
+       "apihelp-compare-param-tocontentformat": "<kbd>toslots=main</kbd>을 지정하고 <var>tocontentformat-main</var>을 대신 사용합니다.",
        "apihelp-compare-param-tosection": "지정된 'to' 내용의 지정된 문단만 사용합니다.",
        "apihelp-compare-param-prop": "가져올 정보입니다.",
        "apihelp-compare-paramvalue-prop-diff": "HTML의 차이입니다.",
index 5d6d431..e5a235c 100644 (file)
@@ -68,6 +68,8 @@
        "apihelp-compare-param-fromid": "Primeiro ID de página para comparar.",
        "apihelp-compare-param-fromrev": "Primeira revisão para comparar.",
        "apihelp-compare-param-frompst": "Faz uma transformação pré-salvar em <var>fromtext</var>.",
+       "apihelp-compare-param-fromtext-{slot}": "Texto do slot especificado. Se omitido, o slot é removido da revisão.",
+       "apihelp-compare-param-fromcontentformat-{slot}": "Formato de serialização de conteúdo de <var>fromtext-&#x7B;slot}</var>.",
        "apihelp-compare-param-fromtext": "Use este texto em vez do conteúdo da revisão especificada por <var>fromtitle</var>, <var>fromid</var> ou <var>fromrev</var>.",
        "apihelp-compare-param-fromcontentmodel": "Modelo de conteúdo de <var>fromtext</var>. Se não for fornecido, será adivinhado com base nos outros parâmetros.",
        "apihelp-compare-param-fromcontentformat": "Formato de serialização de conteúdo de <var>fromtext</var>.",
@@ -77,6 +79,7 @@
        "apihelp-compare-param-torev": "Segunda revisão para comparar.",
        "apihelp-compare-param-torelative": "Use uma revisão relativa à revisão determinada de <var>fromtitle</var>, <var>fromid</var> ou <var>fromrev</var>. Todas as outras opções 'to' serão ignoradas.",
        "apihelp-compare-param-topst": "Faz uma transformação pré-salvar em <var>totext</var>.",
+       "apihelp-compare-param-totext-{slot}": "Texto do slot especificado.",
        "apihelp-compare-param-totext": "Use este texto em vez do conteúdo da revisão especificada por <var>totitle</var>, <var>toid</var> ou <var>torev</var>.",
        "apihelp-compare-param-tocontentmodel": "Modelo de conteúdo de <var>totext</var>. Se não for fornecido, será adivinhado com base nos outros parâmetros.",
        "apihelp-compare-param-tocontentformat": "Formato de serialização de conteúdo de <var>totext</var>.",
@@ -91,6 +94,7 @@
        "apihelp-compare-paramvalue-prop-comment": "O comentário das revisões 'from' e 'to'.",
        "apihelp-compare-paramvalue-prop-parsedcomment": "O comentário analisado sobre as revisões 'from' e 'to'.",
        "apihelp-compare-paramvalue-prop-size": "O tamanho das revisões 'from' e 'to'.",
+       "apihelp-compare-param-slots": "Devolve os diffs individuais para estes slots, em vez de um diff combinado para todos os slots.",
        "apihelp-compare-example-1": "Criar um diff entre a revisão 1 e 2.",
        "apihelp-createaccount-summary": "Criar uma nova conta de usuário.",
        "apihelp-createaccount-param-preservestate": "Se <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> retornar true para <samp>hasprimarypreservedstate</samp>, pedidos marcados como <samp>hasprimarypreservedstate</samp> devem ser omitidos. Se retornou um valor não vazio para <samp>preservedusername</samp>, esse nome de usuário deve ser usado pelo parâmetro <var>username</var>.",
        "apierror-mimesearchdisabled": "A pesquisa MIME está desativada no Miser Mode.",
        "apierror-missingcontent-pageid": "Falta conteúdo para a ID da página $1.",
        "apierror-missingcontent-revid": "Falta conteúdo para a ID de revisão $1.",
+       "apierror-missingcontent-revid-role": "Conteúdo ausente para o ID de revisão $1 para a função $2.",
        "apierror-missingparam-at-least-one-of": "{{PLURAL:$2|O parâmetro|Ao menos um dos parâmetros}} $1 é necessário.",
        "apierror-missingparam-one-of": "{{PLURAL:$2|O parâmetro|Um dos parâmetros}} $1 é necessário.",
        "apierror-missingparam": "O parâmetro <var>$1</var> precisa ser definido.",
index bf4c63e..c157ec0 100644 (file)
        "apihelp-compare-param-fromtitle": "Primeiro título a comparar.",
        "apihelp-compare-param-fromid": "Primeiro identificador de página a comparar.",
        "apihelp-compare-param-fromrev": "Primeira revisão a comparar.",
-       "apihelp-compare-param-frompst": "Fazer uma transformação anterior à gravação, de <var>fromtext</var>.",
-       "apihelp-compare-param-fromtext": "Usar este texto em vez do conteúdo da revisão especificada por <var>fromtitle</var>, <var>fromid</var> ou <var>fromrev</var>.",
-       "apihelp-compare-param-fromcontentmodel": "Modelo de conteúdo de <var>fromtext</var>. Se não for fornecido, ele será deduzido a partir dos outros parâmetros.",
-       "apihelp-compare-param-fromcontentformat": "Formato de seriação do conteúdo de <var>fromtext</var>.",
+       "apihelp-compare-param-frompst": "Fazer uma transformação anterior à gravação, de <var>fromtext-&#x7B;slot}</var>.",
+       "apihelp-compare-param-fromslots": "Substituir o conteúdo da revisão especificada por <var>fromtitle</var>, <var>fromid</var> ou <var>fromrev</var>.\n\nEste parâmetro especifica os segmentos que deverão ser modificados. Use <var>fromtext-&#x7B;slot}</var>, <var>fromcontentmodel-&#x7B;slot}</var> e <var>fromcontentformat-&#x7B;slot}</var> para especificar conteúdo para cada segmento.",
+       "apihelp-compare-param-fromtext-{slot}": "Texto do segmento especificado. Se for omitido, o segmento é removido da revisão.",
+       "apihelp-compare-param-fromsection-{slot}": "Quando <var>fromtext-&#x7B;slot}</var> é o conteúdo de uma única secção, este é o número da secção. Será fundido na revisão especificada por <var>fromtitle</var>, <var>fromid</var> ou <var>fromrev</var> tal como acontece na edição de uma secção.",
+       "apihelp-compare-param-fromcontentmodel-{slot}": "Modelo de conteúdo de <var>fromtext-&#x7B;slot}</var>. Se não for fornecido, ele será deduzido a partir dos outros parâmetros.",
+       "apihelp-compare-param-fromcontentformat-{slot}": "Formato de seriação do conteúdo de <var>fromtext-&#x7B;slot}</var>.",
+       "apihelp-compare-param-fromtext": "Especificar <kbd>fromslots=main</kbd> e usar <var>fromtext-main</var>.",
+       "apihelp-compare-param-fromcontentmodel": "Especificar <kbd>fromslots=main</kbd> e usar <var>fromcontentmodel-main</var>.",
+       "apihelp-compare-param-fromcontentformat": "Especificar <kbd>fromslots=main</kbd> e usar <var>fromcontentformat-main</var>.",
        "apihelp-compare-param-fromsection": "Utilizar apenas a secção especificada do conteúdo 'from' especificado.",
        "apihelp-compare-param-totitle": "Segundo título a comparar.",
        "apihelp-compare-param-toid": "Segundo identificador de página a comparar.",
        "apihelp-compare-param-torev": "Segunda revisão a comparar.",
        "apihelp-compare-param-torelative": "Usar uma revisão relativa à revisão determinada a partir de <var>fromtitle</var>, <var>fromid</var> ou <var>fromrev</var>. Todas as outras opções 'to' serão ignoradas.",
        "apihelp-compare-param-topst": "Fazer uma transformação anterior à gravação, de <var>totext</var>.",
-       "apihelp-compare-param-totext": "Usar este texto em vez do conteúdo da revisão especificada por <var>totitle</var>, <var>toid</var> ou <var>torev</var>.",
-       "apihelp-compare-param-tocontentmodel": "Modelo de conteúdo de <var>totext</var>. Se não for fornecido, ele será deduzido a partir dos outros parâmetros.",
-       "apihelp-compare-param-tocontentformat": "Formato de seriação do conteúdo de <var>totext</var>.",
+       "apihelp-compare-param-toslots": "Especificar o conteúdo para ser usado em vez do conteúdo da revisão especificada em <var>totitle</var>, <var>toid</var> ou <var>torev</var>.\n\nEste parâmetro especifica os segmentos que têm conteúdo. Use <var>totext-&#x7B;slot}</var>, <var>tocontentmodel-&#x7B;slot}</var> e <var>tocontentformat-&#x7B;slot}</var> para especificar conteúdo para cada segmento.",
+       "apihelp-compare-param-totext-{slot}": "Texto do segmento especificado.",
+       "apihelp-compare-param-tosection-{slot}": "Quando <var>totext-&#x7B;slot}</var> é o conteúdo de uma única secção, este é o número da secção. Será fundido na revisão especificada por <var>totitle</var>, <var>toid</var> ou <var>torev</var> tal como acontece na edição de uma secção.",
+       "apihelp-compare-param-tocontentmodel-{slot}": "Modelo de conteúdo de <var>totext-&#x7B;slot}</var>. Se não for fornecido, ele será deduzido a partir dos outros parâmetros.",
+       "apihelp-compare-param-tocontentformat-{slot}": "Formato de seriação do conteúdo de <var>totext-&#x7B;slot}</var>.",
+       "apihelp-compare-param-totext": "Especificar <kbd>toslots=main</kbd> e usar <var>totext-main</var>.",
+       "apihelp-compare-param-tocontentmodel": "Especificar <kbd>toslots=main</kbd> e usar <var>tocontentmodel-main</var>.",
+       "apihelp-compare-param-tocontentformat": "Especificar <kbd>toslots=main</kbd> e usar <var>tocontentformat-main</var>.",
        "apihelp-compare-param-tosection": "Utilizar apenas a secção especificada do conteúdo 'to' especificado.",
        "apihelp-compare-param-prop": "As informações que devem ser obtidas.",
        "apihelp-compare-paramvalue-prop-diff": "O HTML da lista de diferenças.",
@@ -86,6 +96,7 @@
        "apihelp-compare-paramvalue-prop-comment": "O comentário das revisões 'from' e 'to'.",
        "apihelp-compare-paramvalue-prop-parsedcomment": "O comentário após análise sintática, das revisões 'from' e 'to'.",
        "apihelp-compare-paramvalue-prop-size": "O tamanho das revisões 'from' e 'to'.",
+       "apihelp-compare-param-slots": "Devolver as diferenças individuais destes segmentos, em vez de uma lista combinada para todos os segmentos.",
        "apihelp-compare-example-1": "Criar uma lista de diferenças entre as revisões 1 e 2.",
        "apihelp-createaccount-summary": "Criar uma conta de utilizador nova.",
        "apihelp-createaccount-param-preservestate": "Se <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> devolveu o valor verdadeiro para <samp>hasprimarypreservedstate</samp>, pedidos marcados como <samp>primary-required</samp> devem ser omitidos. Se devolveu um valor não vazio em <samp>preservedusername</samp>, esse nome de utilizador tem de ser usado no parâmetro <var>username</var>.",
        "apierror-compare-no-title": "Não é possível transformar antes da gravação, sem um título. Tente especificar <var>fromtitle</var> ou <var>totitle</var>.",
        "apierror-compare-nosuchfromsection": "Não há nenhuma secção $1 no conteúdo 'from'.",
        "apierror-compare-nosuchtosection": "Não há nenhuma secção $1 no conteúdo 'to'.",
+       "apierror-compare-nofromrevision": "Não foi especificada uma revisão 'from'. Especificar <var>fromrev</var>, <var>fromtitle</var> ou <var>fromid</var>.",
+       "apierror-compare-notorevision": "Não foi especificada uma revisão 'to'. Especificar <var>torev</var>, <var>totitle</var> ou <var>toid</var>.",
        "apierror-compare-relative-to-nothing": "Não existe uma revisão 'from' em relação à qual <var>torelative</var> possa ser relativo.",
        "apierror-contentserializationexception": "A seriação do conteúdo falhou: $1",
        "apierror-contenttoobig": "O conteúdo que forneceu excede o tamanho máximo dos artigos que é $1 {{PLURAL:$1|kilobyte|kilobytes}}.",
        "apierror-mimesearchdisabled": "A pesquisa MIME é desativada no modo avarento.",
        "apierror-missingcontent-pageid": "Conteúdo em falta para a página com o identificador $1.",
        "apierror-missingcontent-revid": "Conteúdo em falta para a revisão com o identificador $1.",
+       "apierror-missingcontent-revid-role": "O identificador de revisão $1 para a função $2 não tem conteúdo.",
        "apierror-missingparam-at-least-one-of": "{{PLURAL:$2|O parâmetro|Pelo menos um dos parâmetros}} $1 é obrigatório.",
        "apierror-missingparam-one-of": "{{PLURAL:$2|O parâmetro|Um dos parâmetros}} $1 é obrigatório.",
        "apierror-missingparam": "O parâmetro <var>$1</var> tem de ser definido.",
index df0e018..4450b6c 100644 (file)
@@ -98,6 +98,7 @@
        "apihelp-compare-param-torev": "Вторая сравниваемая версия.",
        "apihelp-compare-param-torelative": "Использовать версию, относящуюся к определённой <var>fromtitle</var>, <var>fromid</var> или <var>fromrev</var>. Все другие опции 'to' будут проигнорированы.",
        "apihelp-compare-param-topst": "Выполнить преобразование перед записью правки (PST) над <var>totext</var>.",
+       "apihelp-compare-param-tocontentmodel-{slot}": "Модель содержимого <var>totext-&#x7B;slot}</var>. Если не задана, будет угадана по другим параметрам.",
        "apihelp-compare-param-totext": "Используйте этот текст вместо содержимого версии, заданной <var>totitle</var>, <var>toid</var> или <var>torev</var>.",
        "apihelp-compare-param-tocontentmodel": "Модель содержимого <var>totext</var>. Если не задана, будет угадана по другим параметрам.",
        "apihelp-compare-param-tocontentformat": "Формат сериализации содержимого <var>totext</var>.",
index 20dc919..1cb6f5c 100644 (file)
@@ -16,7 +16,8 @@
                        "Rockyfelle",
                        "Macofe",
                        "Magol",
-                       "Bengtsson96"
+                       "Bengtsson96",
+                       "Larske"
                ]
        },
        "apihelp-main-extended-description": "<div class=\"hlist plainlinks api-main-links\">\n* [[mw:Special:MyLanguage/API:Main_page|Dokumentation]]\n* [[mw:Special:MyLanguage/API:FAQ|Vanliga frågor]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Sändlista]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API-nyheter]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Buggar och begäran]\n</div>\n<strong>Status:</strong> Alla funktioner som visas på denna sida bör fungera, men API:et är fortfarande under utveckling och kan ändras när som helst. Prenumerera på [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ sändlistan mediawiki-api-announce] för uppdateringsaviseringar.\n\n<strong>Felaktiga begäran:</strong> När felaktiga begäran skickas till API:et kommer en HTTP-header skickas med nyckeln \"MediaWiki-API-Error\" och sedan kommer både värdet i headern och felkoden som skickades tillbaka anges som samma värde. För mer information se [[mw:Special:MyLanguage/API:Errors_and_warnings|API: Fel och varningar]].\n\n<p class=\"mw-apisandbox-link\"><strong>Testning:</strong> För enkelt testning av API-begäran, se [[Special:ApiSandbox]].</p>",
        "apihelp-query+images-param-limit": "Hur många filer att returnera.",
        "apihelp-query+images-param-dir": "Riktningen att lista mot.",
        "apihelp-query+images-example-simple": "Hämta en lista över filer som används på [[Main Page]].",
-       "apihelp-query+imageusage-summary": "Hitta alla sidor som användare angiven bildtitel.",
+       "apihelp-query+imageusage-summary": "Hitta alla sidor som använder angiven bildtitel.",
        "apihelp-query+imageusage-param-dir": "Riktningen att lista mot.",
        "apihelp-query+imageusage-example-simple": "Visa sidor med hjälp av [[:File:Albert Einstein Head.jpg]].",
        "apihelp-query+imageusage-example-generator": "Hämta information om sidor med hjälp av [[:File:Albert Einstein Head.jpg]].",
index cf29d27..5cba292 100644 (file)
        "apihelp-query+redirects-example-generator": "获取所有重定向至[[Main Page]]的信息。",
        "apihelp-query+revisions-summary": "获取修订版本信息。",
        "apihelp-query+revisions-extended-description": "可用于以下几个方面:\n# 通过设置标题或页面ID获取一批页面(最新修订)的数据。\n# 通过使用带start、end或limit的标题或页面ID获取给定页面的多个修订。\n# 通过revid设置一批修订的ID获取它们的数据。",
-       "apihelp-query+revisions-paraminfo-singlepageonly": "å\8f¯è\83½å\8fªè\83½ä¸\8eå\8d\95ä¸\80页é\9d¢使用(模式#2)。",
+       "apihelp-query+revisions-paraminfo-singlepageonly": "å\8fªè\83½å\9c¨å\8d\95ä¸\80页é\9d¢æ¨¡å¼\8f中使用(模式#2)。",
        "apihelp-query+revisions-param-startid": "从这个修订版本时间戳开始列举。修订版本必须存在,但未必与该页面相关。",
        "apihelp-query+revisions-param-endid": "在这个修订版本时间戳停止列举。修订版本必须存在,但未必与该页面相关。",
        "apihelp-query+revisions-param-start": "从哪个修订版本时间戳开始列举。",
index 82b3ea8..9f80a78 100644 (file)
        "apihelp-query+categories-summary": "列出頁面隸屬的所有分類。",
        "apihelp-query+categories-param-show": "要顯示出的分類種類。",
        "apihelp-query+categories-param-limit": "要回傳的分類數量。",
+       "apihelp-query+categories-param-dir": "列出時所採用的方向。",
        "apihelp-query+categoryinfo-summary": "回傳有關指定分類的資訊。",
        "apihelp-query+categorymembers-summary": "在指定的分類中列出所有頁面。",
        "apihelp-query+categorymembers-param-prop": "要包含的資訊部份:",
        "apihelp-query+imageinfo-paramvalue-prop-mime": "替檔案添加 MIME 類型。",
        "apihelp-query+imageinfo-paramvalue-prop-mediatype": "添加檔案的媒體類型。",
        "apihelp-query+imageinfo-param-limit": "每個檔案要回傳的檔案修訂數量。",
+       "apihelp-query+imageinfo-param-start": "列出的起始時間戳記。",
+       "apihelp-query+imageinfo-param-end": "列出的終止時間戳記。",
+       "apihelp-query+imageinfo-param-urlheight": "與 $1urlwidth 相似。",
        "apihelp-query+images-summary": "回傳指定頁面中包含的所有檔案。",
        "apihelp-query+images-param-limit": "要回傳的檔案數量。",
        "apihelp-query+images-param-dir": "列出時所採用的方向。",
        "apihelp-query+images-example-simple": "取得使用在 [[Main Page]] 的檔案清單。",
+       "apihelp-query+imageusage-param-title": "要搜尋的標題。不能與 $1pageid 一起使用。",
+       "apihelp-query+imageusage-param-pageid": "要搜尋的頁面 ID。不能與 $1title 一起使用。",
        "apihelp-query+imageusage-param-namespace": "要列舉的命名空間。",
        "apihelp-query+imageusage-param-dir": "列出時所採用的方向。",
        "apihelp-query+info-summary": "取得基本頁面訊息。",
        "apihelp-query+info-param-prop": "要取得的額外屬性:",
        "apihelp-query+info-paramvalue-prop-protection": "列出各頁面的保護層級。",
        "apihelp-query+info-paramvalue-prop-readable": "使用者是否可閱讀此頁面。",
+       "apihelp-query+iwbacklinks-param-prefix": "跨 wiki 前綴。",
+       "apihelp-query+iwbacklinks-param-limit": "要回傳的頁面總數。",
        "apihelp-query+iwbacklinks-param-prop": "要取得的屬性。",
+       "apihelp-query+iwbacklinks-paramvalue-prop-iwprefix": "添加跨 wiki 前綴。",
+       "apihelp-query+iwbacklinks-paramvalue-prop-iwtitle": "添加跨 wiki 標題。",
+       "apihelp-query+iwbacklinks-param-dir": "列出時所採用的方向。",
+       "apihelp-query+iwbacklinks-example-simple": "取得連結至 [[wikibooks:Test]] 的頁面。",
        "apihelp-query+iwlinks-summary": "回傳指定頁面的所有 interwiki 連結。",
        "apihelp-query+iwlinks-paramvalue-prop-url": "添加完整的 URL。",
        "apihelp-query+iwlinks-param-limit": "要回傳的跨 Wiki 連結數量。",
        "apihelp-query+iwlinks-param-dir": "列出時所採用的方向。",
+       "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+langlinks-summary": "回傳指定頁面的所有跨語言連結。",
        "apihelp-query+langlinks-param-limit": "要回傳的 langlinks 數量。",
        "apihelp-query+langlinks-paramvalue-prop-url": "添加完整的 URL。",
+       "apihelp-query+langlinks-paramvalue-prop-autonym": "添加本地語言名稱。",
        "apihelp-query+langlinks-param-dir": "列出時所採用的方向。",
        "apihelp-query+langlinks-param-inlanguagecode": "用於本地化語言名稱的語言代碼。",
        "apihelp-query+links-summary": "回傳指定頁面的所有連結。",
        "apihelp-query+links-param-limit": "要回傳的連結數量。",
+       "apihelp-query+links-param-dir": "列出時所採用的方向。",
        "apihelp-query+linkshere-param-prop": "要取得的屬性。",
        "apihelp-query+linkshere-paramvalue-prop-pageid": "各頁面的頁面 ID。",
        "apihelp-query+linkshere-paramvalue-prop-title": "各頁面的標題。",
        "apihelp-query+linkshere-param-limit": "要回傳的數量。",
        "apihelp-query+logevents-summary": "從日誌中獲取事件。",
        "apihelp-query+logevents-param-prop": "要取得的屬性。",
+       "apihelp-query+logevents-paramvalue-prop-ids": "添加日誌事件的 ID。",
        "apihelp-query+logevents-param-start": "起始列舉的時間戳記。",
        "apihelp-query+logevents-param-end": "結束列舉的時間戳記。",
        "apihelp-query+logevents-param-limit": "要回傳的事件項目總數。",
+       "apihelp-query+logevents-example-simple": "列出近期日誌事件。",
        "apihelp-query+pagepropnames-param-limit": "回傳的名稱數量上限。",
        "apihelp-query+pagepropnames-example-simple": "取得前 10 個屬性名稱。",
        "apihelp-query+pageswithprop-paramvalue-prop-ids": "添加頁面 ID。",
+       "apihelp-query+pageswithprop-paramvalue-prop-value": "添加頁面屬性的值。",
        "apihelp-query+pageswithprop-param-limit": "回傳的頁面數量上限。",
        "apihelp-query+prefixsearch-param-search": "搜尋字串。",
        "apihelp-query+prefixsearch-param-namespace": "搜尋的命名空間。若 <var>$1search</var> 以有效的命名空間前綴為開頭則會被忽略。",
        "apihelp-query+redirects-paramvalue-prop-title": "各重新導向的標題。",
        "apihelp-query+redirects-param-namespace": "僅包含這些命名空間的頁面。",
        "apihelp-query+redirects-param-limit": "要回傳的重新導向數量。",
+       "apihelp-query+redirects-example-simple": "取得 [[Main Page]] 的重新導向清單",
        "apihelp-query+revisions-summary": "取得修訂的資訊。",
        "apihelp-query+revisions-example-content": "取得用於標題 <kbd>API</kbd> 與 <kbd>Main Page</kbd> 最新修訂內容的資料。",
        "apihelp-query+revisions-example-last5": "取得 <kbd>Main Page</kbd> 的最近 5 筆修訂。",
        "apihelp-query+revisions-example-first5-not-localhost": "取得 <kbd>Main Page</kbd> 裡並非由匿名使用者 <kbd>127.0.0.1</kbd> 所做出的最早前 5 筆修訂。",
        "apihelp-query+revisions-example-first5-user": "取得 <kbd>Main Page</kbd> 裡由使用者 <kbd>MediaWiki default</kbd> 所做出的最早前 5 筆修訂。",
        "apihelp-query+revisions+base-paramvalue-prop-ids": "修訂 ID。",
+       "apihelp-query+revisions+base-paramvalue-prop-user": "做出修訂的使用者。",
        "apihelp-query+revisions+base-paramvalue-prop-tags": "修訂標籤。",
        "apihelp-query+search-summary": "執行全文搜尋。",
        "apihelp-query+search-param-what": "要執行的搜尋類型。",
        "apihelp-query+search-paramvalue-prop-score": "已忽略",
        "apihelp-query+search-paramvalue-prop-hasrelated": "已忽略",
        "apihelp-query+search-param-limit": "要回傳的頁面總數。",
+       "apihelp-query+search-example-simple": "搜尋 <kbd>meaning</kbd>。",
+       "apihelp-query+search-example-text": "搜尋 <kbd>meaning</kbd> 的文字。",
+       "apihelp-query+siteinfo-param-prop": "要取得的資訊:",
        "apihelp-query+siteinfo-paramvalue-prop-general": "全面系統資訊。",
        "apihelp-query+siteinfo-paramvalue-prop-specialpagealiases": "特殊頁面別名清單。",
        "apihelp-query+siteinfo-param-numberingroup": "列出在使用者群組裡的使用者數目。",
        "apihelp-query+siteinfo-example-simple": "索取站台資訊。",
        "apihelp-query+siteinfo-example-interwiki": "索取本地端跨 wiki 前綴的清單。",
+       "apihelp-query+siteinfo-example-replag": "檢查目前的響應延遲。",
        "apihelp-query+stashimageinfo-summary": "回傳多筆儲藏檔案的檔案資訊。",
        "apihelp-query+stashimageinfo-example-simple": "回傳儲藏檔案的檔案資訊。",
        "apihelp-query+tags-summary": "列出變更標記。",
        "apihelp-query+usercontribs-paramvalue-prop-comment": "添加編輯的註釋。",
        "apihelp-query+usercontribs-paramvalue-prop-parsedcomment": "添加編輯的已解析註解。",
        "apihelp-query+usercontribs-paramvalue-prop-size": "添加編輯的新大小。",
+       "apihelp-query+usercontribs-paramvalue-prop-tags": "列出編輯的標籤。",
        "apihelp-query+userinfo-summary": "取得目前使用者的資訊。",
        "apihelp-query+userinfo-param-prop": "要包含的資訊部份:",
        "apihelp-query+userinfo-paramvalue-prop-realname": "添加使用者的真實姓名。",
        "apihelp-query+users-param-prop": "要包含的資訊部份:",
        "apihelp-query+watchlist-param-start": "起始列舉的時間戳記。",
        "apihelp-query+watchlist-param-end": "結束列舉的時間戳記。",
+       "apihelp-query+watchlist-param-user": "此列出由該使用者作出的更改。",
+       "apihelp-query+watchlist-param-excludeuser": "不要列出由該使用者作出的更改。",
        "apihelp-query+watchlist-param-limit": "每個請求要回傳的結果總數。",
+       "apihelp-query+watchlist-param-prop": "要取得的額外屬性:",
        "apihelp-query+watchlist-paramvalue-prop-title": "添加頁面標題。",
        "apihelp-query+watchlist-paramvalue-prop-flags": "添加編輯的標籤。",
        "apihelp-query+watchlist-paramvalue-prop-tags": "列出項目的標籤。",
+       "apihelp-query+watchlist-paramvalue-type-edit": "一般頁面編輯。",
        "apihelp-query+watchlist-paramvalue-type-new": "頁面建立。",
        "apihelp-query+watchlist-paramvalue-type-log": "日誌項目。",
        "apihelp-query+watchlist-paramvalue-type-categorize": "分類成員更改。",
        "apihelp-query+watchlistraw-param-limit": "每個請求要回傳的結果總數。",
+       "apihelp-query+watchlistraw-param-prop": "要取得的額外屬性:",
        "apihelp-query+watchlistraw-param-dir": "列出時所採用的方向。",
+       "apihelp-query+watchlistraw-example-simple": "列出在目前使用者的監視清單裡頭頁面。",
        "apihelp-removeauthenticationdata-summary": "為目前使用者移除身分核對資料。",
+       "apihelp-resetpassword-summary": "寄送重新設定密碼的電子郵件給使用者。",
        "apihelp-revisiondelete-summary": "刪除和取消刪除修訂。",
        "apihelp-rollback-summary": "撤修頁面的最後一次編輯。",
        "apihelp-setpagelanguage-summary": "更改頁面的語言。",
        "apihelp-setpagelanguage-param-reason": "變更的原因。",
        "apihelp-stashedit-param-title": "正在編輯此頁面的標題。",
        "apihelp-stashedit-param-text": "頁面內容。",
+       "apihelp-tag-param-reason": "變更的原因。",
        "apihelp-tokens-summary": "取得資料修改動作的密鑰。",
        "apihelp-tokens-extended-description": "此模組已因支援 [[Special:ApiHelp/query+tokens|action=query&meta=tokens]] 而停用。",
        "apihelp-unblock-summary": "解除封鎖一位使用者。",
        "apihelp-unblock-example-id": "解除封銷 ID #<kbd>105</kbd>。",
        "apihelp-undelete-param-title": "要恢復的頁面標題。",
        "apihelp-undelete-param-reason": "還原的原因。",
+       "apihelp-undelete-example-page": "取消刪除頁面 <kbd>Main Page</kbd>。",
+       "apihelp-undelete-example-revisions": "取消刪除 <kbd>Main Page</kbd> 的兩筆修訂。",
        "apihelp-upload-param-filename": "目標檔案名稱。",
        "apihelp-upload-param-comment": "上傳註釋。如果 <var>$1text</var> 未指定的話,也會作為新檔案用的初始頁面文字。",
+       "apihelp-upload-param-text": "用於新檔案的初始頁面文字。",
        "apihelp-upload-param-watch": "監視頁面。",
        "apihelp-upload-param-ignorewarnings": "忽略所有警告。",
        "apihelp-upload-param-file": "檔案內容。",
+       "apihelp-upload-param-url": "索取檔案的來源 URL。",
        "apihelp-upload-example-url": "從 URL 上傳。",
        "apihelp-userrights-summary": "變更一位使用者的群組成員。",
        "apihelp-userrights-param-user": "使用者名稱。",
        "apihelp-userrights-param-remove": "從這些群組移除使用者。",
        "apihelp-userrights-param-reason": "變更的原因。",
        "apihelp-validatepassword-param-password": "要驗證的密碼。",
+       "apihelp-validatepassword-param-email": "電子郵件地址,用於當測試帳號建立時使用。",
+       "apihelp-validatepassword-param-realname": "真實姓名,用於當測試帳號建立時使用。",
        "apihelp-watch-example-watch": "監視頁面 <kbd>Main Page</kbd>。",
+       "apihelp-watch-example-unwatch": "取消監視頁面 <kbd>Main Page</kbd>。",
        "apihelp-format-example-generic": "以 $1 格式傳回查詢結果。",
        "apihelp-json-summary": "使用 JSON 格式輸出資料。",
        "apihelp-jsonfm-summary": "使用 JSON 格式輸出資料 (使用 HTML 格式顯示)。",
        "apihelp-phpfm-summary": "使用序列化 PHP 格式輸出資料 (使用 HTML 格式顯示)。",
        "apihelp-rawfm-summary": "使用 JSON 格式的除錯元素輸出資料 (使用 HTML 格式顯示)。",
        "apihelp-xml-summary": "使用 XML 格式輸出資料。",
+       "apihelp-xml-param-includexmlnamespace": "若有指定,添加一個 XML 命名空間。",
        "apihelp-xmlfm-summary": "使用 XML 格式輸出資料 (使用 HTML 格式顯示)。",
        "api-format-title": "MediaWiki API 結果",
        "api-format-prettyprint-header": "這是$1格式的HTML呈現。HTML適合用於除錯,但不適合應用程式使用。\n\n指定<var>format</var>參數以更改輸出格式。要檢視$1格式的非HTML呈現,設定<kbd>format=$2</kbd>。\n\n參考 [[mw:Special:MyLanguage/API|完整說明文件]] 或 [[Special:ApiHelp/main|API說明]] 以取得更多資訊。",
        "api-help-title": "MediaWiki API 說明",
        "api-help-lead": "此頁為自動產生的 MediaWiki API 說明文件頁面。\n\n說明文件與範例:https://www.mediawiki.org/wiki/API",
        "api-help-main-header": "主要模組",
+       "api-help-undocumented-module": "沒有用於模組 $1 的說明文件。",
        "api-help-flag-deprecated": "此模組已停用。",
        "api-help-flag-internal": "<strong>此模組是內部的或不穩定的。</strong>它的操作可能更改而不另行通知。",
        "api-help-flag-readrights": "此模組需要讀取權限。",
        "api-help-authmanagerhelper-returnurl": "為第三方身份驗證流程傳回URL,必須為絕對值。需要此值或<var>$1continue</var>兩者之一。\n\n在接收<samp>REDIRECT</samp>回應時,一般狀況下您將打開瀏覽器或網站瀏覽功能到特定的<samp>redirecttarget</samp> URL以進行第三方身份驗證流程。當它完成時,第三方會將瀏覽器或網站瀏覽功能送至此URL。您應當提取任何來自URL的查詢或POST參數,並將之作為<var>$1continue</var>請求傳遞至此API模組。",
        "api-help-authmanagerhelper-continue": "此請求是在先前的<samp>UI</samp>或<samp>REDIRECT</samp>回應之後的後續動作。必須為此值或<var>$1returnurl</var>。",
        "api-help-authmanagerhelper-additional-params": "此模組允許額外參數,取決於可用的身份驗證請求。使用<kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd>与<kbd>amirequestsfor=$1</kbd>(或之前來自此模組的回應,如果合適)以決定可用請求及其使用的欄位。",
+       "apierror-badgenerator-unknown": "未知的 <kbd>generator=$1</kbd>。",
        "apierror-badip": "IP 參數無效。",
        "apierror-badmd5": "提供的 MD5 雜湊不正確。",
        "apierror-badquery": "無效的查詢。",
+       "apierror-cantblock": "您沒有權限來解封使用者。",
+       "apierror-cantimport": "您沒有權限來匯入頁面。",
+       "apierror-changeauth-norequest": "建立更改請求失敗。",
+       "apierror-contentserializationexception": "內容序列化失敗:$1",
        "apierror-copyuploadbadurl": "不允許從此 URL 來上傳。",
+       "apierror-csp-report": "處理 CSP 報告時錯誤:$1。",
        "apierror-filedoesnotexist": "檔案不存在。",
        "apierror-filenopath": "無法取得本地端檔案路徑。",
        "apierror-filetypecannotberotated": "無法旋轉的檔案類型。",
        "apierror-imageusage-badtitle": "<kbd>$1</kbd>的標題必須是檔案。",
        "apierror-import-unknownerror": "未知的匯入錯誤:$1",
        "apierror-invalidsha1hash": "所提供的 SHA1 雜湊無效。",
+       "apierror-invalidtitle": "錯誤標題「$1」。",
        "apierror-invaliduser": "無效的使用者名稱「$1」。",
        "apierror-invaliduserid": "使用者 ID <var>$1</var> 無效。",
        "apierror-missingparam": "<var>$1</var>參數必須被設定。",
        "apierror-mustbeloggedin-generic": "您必須登入。",
        "apierror-mustbeloggedin-linkaccounts": "您必須登入到連結帳號。",
        "apierror-mustbeloggedin-removeauth": "必須登入,才能移除身分核對資取。",
+       "apierror-mustbeloggedin": "您必須登入至$1。",
        "apierror-nodeleteablefile": "沒有這樣檔案的舊版本。",
        "apierror-noedit-anon": "匿名使用者不可編輯頁面。",
        "apierror-noedit": "您沒有權限來編輯頁面。",
        "apierror-permissiondenied": "您沒有權限$1。",
        "apierror-permissiondenied-generic": "權限不足。",
        "apierror-permissiondenied-unblock": "您沒有權限來解封使用者。",
+       "apierror-protect-invalidaction": "無效的保護類型「$1」。",
+       "apierror-protect-invalidlevel": "無效的保護層級「$1」。",
        "apierror-readapidenied": "您需要有閱讀權限來使用此模組。",
        "apierror-readonly": "Wiki 目前為唯讀模式。",
        "apierror-reauthenticate": "於本工作階段還未核對身分,請重新核對。",
index b3286a9..22fbe6f 100644 (file)
@@ -1,9 +1,4 @@
 <?php
-
-use MediaWiki\Logger\LoggerFactory;
-use MediaWiki\MediaWikiServices;
-use MediaWiki\Search\ParserOutputSearchDataExtractor;
-
 /**
  * Base class for content handling.
  *
@@ -29,6 +24,12 @@ use MediaWiki\Search\ParserOutputSearchDataExtractor;
  *
  * @author Daniel Kinzler
  */
+
+use Wikimedia\Assert\Assert;
+use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Search\ParserOutputSearchDataExtractor;
+
 /**
  * A content handler knows how do deal with a specific type of content on a wiki
  * page. Content is stored in the database in a serialized form (using a
@@ -1129,31 +1130,52 @@ abstract class ContentHandler {
         * must exist and must not be deleted.
         *
         * @since 1.21
+        * @since 1.32 accepts Content objects for all parameters instead of Revision objects.
+        *  Passing Revision objects is deprecated.
         *
-        * @param Revision $current The current text
-        * @param Revision $undo The revision to undo
-        * @param Revision $undoafter Must be an earlier revision than $undo
+        * @param Revision|Content $current The current text
+        * @param Revision|Content $undo The content of the revision to undo
+        * @param Revision|Content $undoafter Must be from an earlier revision than $undo
+        * @param bool $undoIsLatest Set true if $undo is from the current revision (since 1.32)
         *
         * @return mixed Content on success, false on failure
         */
-       public function getUndoContent( Revision $current, Revision $undo, Revision $undoafter ) {
-               $cur_content = $current->getContent();
+       public function getUndoContent( $current, $undo, $undoafter, $undoIsLatest = false ) {
+               Assert::parameterType( Revision::class . '|' . Content::class, $current, '$current' );
+               if ( $current instanceof Content ) {
+                       Assert::parameter( $undo instanceof Content, '$undo',
+                               'Must be Content when $current is Content' );
+                       Assert::parameter( $undoafter instanceof Content, '$undoafter',
+                               'Must be Content when $current is Content' );
+                       $cur_content = $current;
+                       $undo_content = $undo;
+                       $undoafter_content = $undoafter;
+               } else {
+                       Assert::parameter( $undo instanceof Revision, '$undo',
+                               'Must be Revision when $current is Revision' );
+                       Assert::parameter( $undoafter instanceof Revision, '$undoafter',
+                               'Must be Revision when $current is Revision' );
 
-               if ( empty( $cur_content ) ) {
-                       return false; // no page
-               }
+                       $cur_content = $current->getContent();
 
-               $undo_content = $undo->getContent();
-               $undoafter_content = $undoafter->getContent();
+                       if ( empty( $cur_content ) ) {
+                               return false; // no page
+                       }
+
+                       $undo_content = $undo->getContent();
+                       $undoafter_content = $undoafter->getContent();
+
+                       if ( !$undo_content || !$undoafter_content ) {
+                               return false; // no content to undo
+                       }
 
-               if ( !$undo_content || !$undoafter_content ) {
-                       return false; // no content to undo
+                       $undoIsLatest = $current->getId() === $undo->getId();
                }
 
                try {
                        $this->checkModelID( $cur_content->getModel() );
                        $this->checkModelID( $undo_content->getModel() );
-                       if ( $current->getId() !== $undo->getId() ) {
+                       if ( !$undoIsLatest ) {
                                // If we are undoing the most recent revision,
                                // its ok to revert content model changes. However
                                // if we are undoing a revision in the middle, then
index acf6fcb..9817c3f 100644 (file)
@@ -296,6 +296,7 @@ class DerivativeContext extends ContextSource implements MutableContext {
        public function msg( $key ) {
                $args = func_get_args();
 
+               // phpcs:ignore MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage
                return wfMessage( ...$args )->setContext( $this );
        }
 }
index 1564fab..5f09555 100644 (file)
@@ -52,6 +52,9 @@ class CloneDatabase {
        public function __construct( IMaintainableDatabase $db, array $tablesToClone,
                $newTablePrefix, $oldTablePrefix = null, $dropCurrentTables = true
        ) {
+               if ( !$tablesToClone ) {
+                       throw new InvalidArgumentException( 'Empty list of tables to clone' );
+               }
                $this->db = $db;
                $this->tablesToClone = $tablesToClone;
                $this->newTablePrefix = $newTablePrefix;
index 891f0fe..0254458 100644 (file)
@@ -57,29 +57,41 @@ class DifferenceEngine extends ContextSource {
         */
        const DIFF_VERSION = '1.12';
 
-       /** @var int Revision ID or 0 for current */
+       /**
+        * Revision ID for the old revision. 0 for the revision previous to $mNewid, false
+        * if the diff does not have an old revision (e.g. 'oldid=<first revision of page>&diff=prev'),
+        * or the revision does not exist, null if the revision is unsaved.
+        * @var int|false|null
+        */
        protected $mOldid;
 
-       /** @var int|string Revision ID or null for current or an alias such as 'next' */
+       /**
+        * Revision ID for the new revision. 0 for the last revision of the current page
+        * (as defined by the request context), false if the revision does not exist, null
+        * if it is unsaved, or an alias such as 'next'.
+        * @var int|string|false|null
+        */
        protected $mNewid;
 
-       private $mOldTags;
-       private $mNewTags;
-
        /**
         * Old revision (left pane).
         * Allowed to be an unsaved revision, unlikely that's ever needed though.
-        * Null when the old revision does not exist; this can happen when using
-        * diff=prev on the first revision.
+        * False when the old revision does not exist; this can happen when using
+        * diff=prev on the first revision. Null when the revision should exist but
+        * doesn't (e.g. load failure); loadRevisionData() will return false in that
+        * case. Also null until lazy-loaded. Ignored completely when isContentOverridden
+        * is set.
         * Since 1.32 public access is deprecated.
-        * @var Revision|null
+        * @var Revision|null|false
         */
        protected $mOldRev;
 
        /**
         * New revision (right pane).
         * Note that this might be an unsaved revision (e.g. for edit preview).
-        * Null only in case of load failure; diff methods will just return an error message in that case.
+        * Null in case of load failure; diff methods will just return an error message in that case,
+        * and loadRevisionData() will return false. Also null until lazy-loaded. Ignored completely
+        * when isContentOverridden is set.
         * Since 1.32 public access is deprecated.
         * @var Revision|null
         */
@@ -99,6 +111,18 @@ class DifferenceEngine extends ContextSource {
         */
        protected $mNewPage;
 
+       /**
+        * Change tags of $mOldRev or null if it does not exist / is not saved.
+        * @var string[]|null
+        */
+       private $mOldTags;
+
+       /**
+        * Change tags of $mNewRev or null if it does not exist / is not saved.
+        * @var string[]|null
+        */
+       private $mNewTags;
+
        /**
         * @var Content|null
         * @deprecated since 1.32, content slots are now handled by the corresponding SlotDiffRenderer.
@@ -244,7 +268,7 @@ class DifferenceEngine extends ContextSource {
        /**
         * Get the old and new content objects for all slots.
         * This method does not do any permission checks.
-        * @return array [ role => [ 'old' => SlotRecord, 'new' => SlotRecord ], ... ]
+        * @return array [ role => [ 'old' => SlotRecord|null, 'new' => SlotRecord|null ], ... ]
         */
        protected function getSlotContents() {
                if ( $this->isContentOverridden ) {
@@ -254,16 +278,21 @@ class DifferenceEngine extends ContextSource {
                                        'new' => $this->mNewContent,
                                ]
                        ];
+               } elseif ( !$this->loadRevisionData() ) {
+                       return [];
                }
 
-               $oldRev = $this->mOldRev->getRevisionRecord();
-               $newRev = $this->mNewRev->getRevisionRecord();
+               $newSlots = $this->mNewRev->getRevisionRecord()->getSlots()->getSlots();
+               if ( $this->mOldRev ) {
+                       $oldSlots = $this->mOldRev->getRevisionRecord()->getSlots()->getSlots();
+               } else {
+                       $oldSlots = [];
+               }
                // The order here will determine the visual order of the diff. The current logic is
-               // changed first, then added, then deleted. This is ad hoc and should not be relied on
-               // - in the future we may want the ordering to depend on the page type.
-               $roles = array_merge( $newRev->getSlotRoles(), $oldRev->getSlotRoles() );
-               $oldSlots = $oldRev->getSlots()->getSlots();
-               $newSlots = $newRev->getSlots()->getSlots();
+               // slots of the new revision first in natural order, then deleted ones. This is ad hoc
+               // and should not be relied on - in the future we may want the ordering to depend
+               // on the page type.
+               $roles = array_merge( array_keys( $newSlots ), array_keys( $oldSlots ) );
 
                $slots = [];
                foreach ( $roles as $role ) {
@@ -311,7 +340,11 @@ class DifferenceEngine extends ContextSource {
        }
 
        /**
-        * @return int
+        * Get the ID of old revision (left pane) of the diff. 0 for the revision
+        * previous to getNewid(), false if the old revision does not exist, null
+        * if it's unsaved.
+        * To get a real revision ID instead of 0, call loadRevisionData() first.
+        * @return int|false|null
         */
        public function getOldid() {
                $this->loadRevisionIds();
@@ -320,7 +353,10 @@ class DifferenceEngine extends ContextSource {
        }
 
        /**
-        * @return bool|int
+        * Get the ID of new revision (right pane) of the diff. 0 for the current revision,
+        * false if the new revision does not exist, null if it's unsaved.
+        * To get a real revision ID instead of 0, call loadRevisionData() first.
+        * @return int|false|null
         */
        public function getNewid() {
                $this->loadRevisionIds();
@@ -1576,7 +1612,8 @@ class DifferenceEngine extends ContextSource {
                        $this->mOldContent = $oldRevision ? $oldRevision->getContent( 'main',
                                RevisionRecord::FOR_THIS_USER, $this->getUser() ) : null;
                } else {
-                       $this->mOldRev = $this->mOldid = $this->mOldPage = null;
+                       $this->mOldPage = null;
+                       $this->mOldRev = $this->mOldid = false;
                }
                $this->mNewRev = new Revision( $newRevision );
                $this->mNewid = $newRevision->getId();
@@ -1610,7 +1647,7 @@ class DifferenceEngine extends ContextSource {
         * @param int $old Revision id, e.g. from URL parameter 'oldid'
         * @param int|string $new Revision id or strings 'next' or 'prev', e.g. from URL parameter 'diff'
         *
-        * @return int[] List of two revision ids, older first, later second.
+        * @return array List of two revision ids, older first, later second.
         *     Zero signifies invalid argument passed.
         *     false signifies that there is no previous/next revision ($old is the oldest/newest one).
         */
@@ -1658,20 +1695,21 @@ class DifferenceEngine extends ContextSource {
        }
 
        /**
-        * Load revision metadata for the specified articles. If newid is 0, then compare
-        * the old article in oldid to the current article; if oldid is 0, then
-        * compare the current article to the immediately previous one (ignoring the
-        * value of newid).
+        * Load revision metadata for the specified revisions. If newid is 0, then compare
+        * the old revision in oldid to the current revision of the current page (as defined
+        * by the request context); if oldid is 0, then compare the revision in newid to the
+        * immediately previous one.
         *
         * If oldid is false, leave the corresponding revision object set
-        * to false. This is impossible via ordinary user input, and is provided for
-        * API convenience.
+        * to false. This can happen with 'diff=prev' pointing to a non-existent revision,
+        * and is also used directly by the API.
         *
-        * @return bool Whether both revisions were loaded successfully.
+        * @return bool Whether both revisions were loaded successfully. Setting mOldRev
+        *   to false counts as successful loading.
         */
        public function loadRevisionData() {
                if ( $this->mRevisionsLoaded ) {
-                       return $this->isContentOverridden || $this->mNewRev && $this->mOldRev;
+                       return $this->isContentOverridden || $this->mNewRev && !is_null( $this->mOldRev );
                }
 
                // Whether it succeeds or fails, we don't want to try again
@@ -1752,12 +1790,16 @@ class DifferenceEngine extends ContextSource {
 
        /**
         * Load the text of the revisions, as well as revision data.
+        * When the old revision is missing (mOldRev is false), loading mOldContent is not attempted.
         *
         * @return bool Whether the content of both revisions could be loaded successfully.
+        *   (When mOldRev is false, that still counts as a success.)
+        *
         */
        public function loadText() {
                if ( $this->mTextLoaded == 2 ) {
-                       return $this->loadRevisionData() && $this->mOldContent && $this->mNewContent;
+                       return $this->loadRevisionData() && ( $this->mOldRev === false || $this->mOldContent )
+                               && $this->mNewContent;
                }
 
                // Whether it succeeds or fails, we don't want to try again
@@ -1774,12 +1816,10 @@ class DifferenceEngine extends ContextSource {
                        }
                }
 
-               if ( $this->mNewRev ) {
-                       $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
-                       Hooks::run( 'DifferenceEngineLoadTextAfterNewContentIsLoaded', [ $this ] );
-                       if ( $this->mNewContent === null ) {
-                               return false;
-                       }
+               $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
+               Hooks::run( 'DifferenceEngineLoadTextAfterNewContentIsLoaded', [ $this ] );
+               if ( $this->mNewContent === null ) {
+                       return false;
                }
 
                return true;
index b00ec3a..7ce125d 100644 (file)
@@ -472,7 +472,7 @@ abstract class IndexPager extends ContextSource implements Pager {
                }
 
                if ( in_array( $type, [ 'asc', 'desc' ] ) ) {
-                       $attrs['title'] = wfMessage( $type == 'asc' ? 'sort-ascending' : 'sort-descending' )->text();
+                       $attrs['title'] = $this->msg( $type == 'asc' ? 'sort-ascending' : 'sort-descending' )->text();
                }
 
                if ( $type ) {
index c603f2f..b05fb0b 100644 (file)
@@ -533,7 +533,7 @@ abstract class Skin extends ContextSource {
                        $t = $embed . implode( "{$pop}{$embed}", $allCats['normal'] ) . $pop;
 
                        $msg = $this->msg( 'pagecategories' )->numParams( count( $allCats['normal'] ) )->escaped();
-                       $linkPage = wfMessage( 'pagecategorieslink' )->inContentLanguage()->text();
+                       $linkPage = $this->msg( 'pagecategorieslink' )->inContentLanguage()->text();
                        $title = Title::newFromText( $linkPage );
                        $link = $title ? Linker::link( $title, $msg ) : $msg;
                        $s .= '<div id="mw-normal-catlinks" class="mw-normal-catlinks">' .
@@ -1331,7 +1331,7 @@ abstract class Skin extends ContextSource {
         * @param string $message
         */
        public function addToSidebar( &$bar, $message ) {
-               $this->addToSidebarPlain( $bar, wfMessage( $message )->inContentLanguage()->plain() );
+               $this->addToSidebarPlain( $bar, $this->msg( $message )->inContentLanguage()->plain() );
        }
 
        /**
@@ -1621,13 +1621,13 @@ abstract class Skin extends ContextSource {
 
                $attribs = [];
                if ( !is_null( $tooltip ) ) {
-                       $attribs['title'] = wfMessage( 'editsectionhint' )->rawParams( $tooltip )
+                       $attribs['title'] = $this->msg( 'editsectionhint' )->rawParams( $tooltip )
                                ->inLanguage( $lang )->text();
                }
 
                $links = [
                        'editsection' => [
-                               'text' => wfMessage( 'editsection' )->inLanguage( $lang )->escaped(),
+                               'text' => $this->msg( 'editsection' )->inLanguage( $lang )->escaped(),
                                'targetTitle' => $nt,
                                'attribs' => $attribs,
                                'query' => [ 'action' => 'edit', 'section' => $section ],
@@ -1652,7 +1652,7 @@ abstract class Skin extends ContextSource {
 
                $result .= implode(
                        '<span class="mw-editsection-divider">'
-                               . wfMessage( 'pipe-separator' )->inLanguage( $lang )->escaped()
+                               . $this->msg( 'pipe-separator' )->inLanguage( $lang )->escaped()
                                . '</span>',
                        $linksHtml
                );
index 970a2e2..1d0ff21 100644 (file)
@@ -157,9 +157,9 @@ class SpecialChangeCredentials extends AuthManagerSpecialPage {
 
                $form->addPreText(
                        Html::openElement( 'dl' )
-                       . Html::element( 'dt', [], wfMessage( 'credentialsform-provider' )->text() )
+                       . Html::element( 'dt', [], $this->msg( 'credentialsform-provider' )->text() )
                        . Html::element( 'dd', [], $info['provider'] )
-                       . Html::element( 'dt', [], wfMessage( 'credentialsform-account' )->text() )
+                       . Html::element( 'dt', [], $this->msg( 'credentialsform-account' )->text() )
                        . Html::element( 'dd', [], $info['account'] )
                        . Html::closeElement( 'dl' )
                );
index f5e2b86..63b64ea 100644 (file)
@@ -706,7 +706,7 @@ class SpecialContributions extends IncludableSpecialPage {
                $dateRangeSelection = Html::rawElement(
                        'div',
                        [],
-                       Xml::label( wfMessage( 'date-range-from' )->text(), 'mw-date-start' ) . ' ' .
+                       Xml::label( $this->msg( 'date-range-from' )->text(), 'mw-date-start' ) . ' ' .
                        new DateInputWidget( [
                                'infusable' => true,
                                'id' => 'mw-date-start',
@@ -714,7 +714,7 @@ class SpecialContributions extends IncludableSpecialPage {
                                'value' => $this->opts['start'],
                                'longDisplayFormat' => true,
                        ] ) . '<br>' .
-                       Xml::label( wfMessage( 'date-range-to' )->text(), 'mw-date-end' ) . ' ' .
+                       Xml::label( $this->msg( 'date-range-to' )->text(), 'mw-date-end' ) . ' ' .
                        new DateInputWidget( [
                                'infusable' => true,
                                'id' => 'mw-date-end',
index 9248a40..7de44d8 100644 (file)
@@ -438,7 +438,7 @@ class SpecialEmailUser extends UnlistedSpecialPage {
                         * SPF and bounce problems with some mailers (see below).
                         */
                        $mailFrom = new MailAddress( $config->get( 'PasswordSender' ),
-                               wfMessage( 'emailsender' )->inContentLanguage()->text() );
+                               $context->msg( 'emailsender' )->inContentLanguage()->text() );
                        $replyTo = $from;
                } else {
                        /**
@@ -482,7 +482,7 @@ class SpecialEmailUser extends UnlistedSpecialPage {
                                if ( $config->get( 'UserEmailUseReplyTo' ) ) {
                                        $mailFrom = new MailAddress(
                                                $config->get( 'PasswordSender' ),
-                                               wfMessage( 'emailsender' )->inContentLanguage()->text()
+                                               $context->msg( 'emailsender' )->inContentLanguage()->text()
                                        );
                                        $replyTo = $ccFrom;
                                } else {
index da10b90..d4ef936 100644 (file)
@@ -42,8 +42,8 @@ class SpecialLinkAccounts extends AuthManagerSpecialPage {
                if ( !$this->isActionAllowed( $this->authAction ) ) {
                        if ( $this->authAction === AuthManager::ACTION_LINK ) {
                                // looks like no linking provider is installed or willing to take this user
-                               $titleMessage = wfMessage( 'cannotlink-no-provider-title' );
-                               $errorMessage = wfMessage( 'cannotlink-no-provider' );
+                               $titleMessage = $this->msg( 'cannotlink-no-provider-title' );
+                               $errorMessage = $this->msg( 'cannotlink-no-provider' );
                                throw new ErrorPageError( $titleMessage, $errorMessage );
                        } else {
                                // user probably back-button-navigated into an auth session that no longer exists
index b159fff..9564d53 100644 (file)
@@ -56,7 +56,7 @@ class SpecialUnlinkAccounts extends AuthManagerSpecialPage {
                }
 
                $status = StatusValue::newGood();
-               $status->warning( wfMessage( 'unlinkaccounts-success' ) );
+               $status->warning( $this->msg( 'unlinkaccounts-success' ) );
                $this->loadAuth( $subPage, null, true ); // update requests so the unlinked one doesn't show up
 
                // Reset sessions - if the user unlinked an account because it was compromised,
index c832f2d..f9d6b5f 100644 (file)
@@ -116,7 +116,7 @@ class SpecialUpload extends SpecialPage {
                $this->mForReUpload = $request->getBool( 'wpForReUpload' ); // updating a file
 
                $commentDefault = '';
-               $commentMsg = wfMessage( 'upload-default-description' )->inContentLanguage();
+               $commentMsg = $this->msg( 'upload-default-description' )->inContentLanguage();
                if ( !$this->mForReUpload && !$commentMsg->isDisabled() ) {
                        $commentDefault = $commentMsg->plain();
                }
@@ -401,12 +401,12 @@ class SpecialUpload extends SpecialPage {
                        } elseif ( $warning == 'no-change' ) {
                                $file = $args;
                                $filename = $file->getTitle()->getPrefixedText();
-                               $msg = "\t<li>" . wfMessage( 'fileexists-no-change', $filename )->parse() . "</li>\n";
+                               $msg = "\t<li>" . $this->msg( 'fileexists-no-change', $filename )->parse() . "</li>\n";
                        } elseif ( $warning == 'duplicate-version' ) {
                                $file = $args[0];
                                $count = count( $args );
                                $filename = $file->getTitle()->getPrefixedText();
-                               $message = wfMessage( 'fileexists-duplicate-version' )
+                               $message = $this->msg( 'fileexists-duplicate-version' )
                                        ->params( $filename )
                                        ->numParams( $count );
                                $msg = "\t<li>" . $message->parse() . "</li>\n";
@@ -415,14 +415,14 @@ class SpecialUpload extends SpecialPage {
                                $ltitle = SpecialPage::getTitleFor( 'Log' );
                                $llink = $linkRenderer->makeKnownLink(
                                        $ltitle,
-                                       wfMessage( 'deletionlog' )->text(),
+                                       $this->msg( 'deletionlog' )->text(),
                                        [],
                                        [
                                                'type' => 'delete',
                                                'page' => Title::makeTitle( NS_FILE, $args )->getPrefixedText(),
                                        ]
                                );
-                               $msg = "\t<li>" . wfMessage( 'filewasdeleted' )->rawParams( $llink )->parse() . "</li>\n";
+                               $msg = "\t<li>" . $this->msg( 'filewasdeleted' )->rawParams( $llink )->parse() . "</li>\n";
                        } elseif ( $warning == 'duplicate' ) {
                                $msg = $this->getDupeWarning( $args );
                        } elseif ( $warning == 'duplicate-archive' ) {
index c8b1578..a00b031 100644 (file)
@@ -130,7 +130,7 @@ class SpecialUploadStash extends UnlistedSpecialPage {
 
                if ( $type !== 'file' && $type !== 'thumb' ) {
                        throw new UploadStashBadPathException(
-                               wfMessage( 'uploadstash-bad-path-unknown-type', $type )
+                               $this->msg( 'uploadstash-bad-path-unknown-type', $type )
                        );
                }
                $fileName = strtok( '/' );
@@ -140,7 +140,7 @@ class SpecialUploadStash extends UnlistedSpecialPage {
                        $srcNamePos = strrpos( $thumbPart, $fileName );
                        if ( $srcNamePos === false || $srcNamePos < 1 ) {
                                throw new UploadStashBadPathException(
-                                       wfMessage( 'uploadstash-bad-path-unrecognized-thumb-name' )
+                                       $this->msg( 'uploadstash-bad-path-unrecognized-thumb-name' )
                                );
                        }
                        $paramString = substr( $thumbPart, 0, $srcNamePos - 1 );
@@ -152,7 +152,7 @@ class SpecialUploadStash extends UnlistedSpecialPage {
                                return [ 'file' => $file, 'type' => $type, 'params' => $params ];
                        } else {
                                throw new UploadStashBadPathException(
-                                       wfMessage( 'uploadstash-bad-path-no-handler', $file->getMimeType(), $file->getPath() )
+                                       $this->msg( 'uploadstash-bad-path-no-handler', $file->getMimeType(), $file->getPath() )
                                );
                        }
                }
@@ -200,14 +200,14 @@ class SpecialUploadStash extends UnlistedSpecialPage {
                $thumbnailImage = $file->transform( $params, $flags );
                if ( !$thumbnailImage ) {
                        throw new UploadStashFileNotFoundException(
-                               wfMessage( 'uploadstash-file-not-found-no-thumb' )
+                               $this->msg( 'uploadstash-file-not-found-no-thumb' )
                        );
                }
 
                // we should have just generated it locally
                if ( !$thumbnailImage->getStoragePath() ) {
                        throw new UploadStashFileNotFoundException(
-                               wfMessage( 'uploadstash-file-not-found-no-local-path' )
+                               $this->msg( 'uploadstash-file-not-found-no-local-path' )
                        );
                }
 
@@ -217,7 +217,7 @@ class SpecialUploadStash extends UnlistedSpecialPage {
                        $this->stash->repo, $thumbnailImage->getStoragePath(), false );
                if ( !$thumbFile ) {
                        throw new UploadStashFileNotFoundException(
-                               wfMessage( 'uploadstash-file-not-found-no-object' )
+                               $this->msg( 'uploadstash-file-not-found-no-object' )
                        );
                }
 
@@ -273,7 +273,7 @@ class SpecialUploadStash extends UnlistedSpecialPage {
                if ( !$status->isOK() ) {
                        $errors = $status->getErrorsArray();
                        throw new UploadStashFileNotFoundException(
-                               wfMessage(
+                               $this->msg(
                                        'uploadstash-file-not-found-no-remote-thumb',
                                        print_r( $errors, 1 ),
                                        $scalerThumbUrl
@@ -283,7 +283,7 @@ class SpecialUploadStash extends UnlistedSpecialPage {
                $contentType = $req->getResponseHeader( "content-type" );
                if ( !$contentType ) {
                        throw new UploadStashFileNotFoundException(
-                               wfMessage( 'uploadstash-file-not-found-missing-content-type' )
+                               $this->msg( 'uploadstash-file-not-found-missing-content-type' )
                        );
                }
 
@@ -302,7 +302,7 @@ class SpecialUploadStash extends UnlistedSpecialPage {
        private function outputLocalFile( File $file ) {
                if ( $file->getSize() > self::MAX_SERVE_BYTES ) {
                        throw new SpecialUploadStashTooLargeException(
-                               wfMessage( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
+                               $this->msg( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
                        );
                }
 
@@ -324,7 +324,7 @@ class SpecialUploadStash extends UnlistedSpecialPage {
                $size = strlen( $content );
                if ( $size > self::MAX_SERVE_BYTES ) {
                        throw new SpecialUploadStashTooLargeException(
-                               wfMessage( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
+                               $this->msg( 'uploadstash-file-too-large', self::MAX_SERVE_BYTES )
                        );
                }
                // Cancel output buffering and gzipping if set
index 3d7148d..889ec1a 100644 (file)
@@ -449,7 +449,7 @@ class ImageListPager extends TablePager {
                                        if ( $thumb ) {
                                                return $thumb->toHtml( [ 'desc-link' => true ] );
                                        } else {
-                                               return wfMessage( 'thumbnail_error', '' )->escaped();
+                                               return $this->msg( 'thumbnail_error', '' )->escaped();
                                        }
                                } else {
                                        return htmlspecialchars( $value );
index 47747b6..57b0cde 100644 (file)
        "confirm-unwatch-top": "Remove this page from your watchlist?",
        "confirm-rollback-button": "OK",
        "confirm-rollback-top": "Revert edits to this page?",
+       "confirm-mcrundo-title": "Undo a change",
+       "mcrundofailed": "Undo failed",
+       "mcrundo-missingparam": "Missing required parameters on request.",
+       "mcrundo-changed": "The page has been changed since you viewed the diff. Please review the new change.",
        "semicolon-separator": ";&#32;",
        "comma-separator": ",&#32;",
        "colon-separator": ":&#32;",
index 7602df5..a33a773 100644 (file)
        "recentchanges-label-minor": "Սա չնչին խմբագրում է",
        "recentchanges-label-bot": "Այս խմբագրումը կատարվել է բոտի կողմից",
        "recentchanges-label-unpatrolled": "Այս խմբագրումը դեռ չի պարեկվել",
-       "recentchanges-label-plusminus": "Էջի չափսը փոփոխվեց այսքան բայթով",
+       "recentchanges-label-plusminus": "Էջի չափսը փոփոխվել է այսքան բայթով",
        "recentchanges-legend-heading": "<strong>Լեգենդ՝</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (տես նաև՝  [[Special:NewPages|նոր էջերի ցանկ]])",
        "recentchanges-submit": "Ցույց տալ",
        "rcfilters-view-tags": "Պիտակված խմբագրումներ",
        "rcfilters-liveupdates-button": "Կենդանի թարմացումներ",
        "rcnotefrom": "Ստորև բերված են փոփոխությունները սկսած՝ '''$2''' (մինչև՝ '''$1''')։",
-       "rclistfrom": "Ցույց տալ նոր փոփոխությունները սկսած $3 $2",
+       "rclistfrom": "Ցույց տալ նոր փոփոխությունները՝ սկսած $3 $2",
        "rcshowhideminor": "$1 չնչին խմբագրումները",
        "rcshowhideminor-show": "Ցուցադրել",
        "rcshowhideminor-hide": "Թաքցնել",
index 297cf75..df2e263 100644 (file)
        "uploadstash-zero-length": "ファイルのサイズがゼロです。",
        "invalid-chunk-offset": "無効なチャンクオフセット",
        "img-auth-accessdenied": "アクセスが拒否されました",
-       "img-auth-nopathinfo": "PATH_INFO が見つかりません。\nサーバーが、この情報を渡すように構成されていません。\nCGI ベースであるため、img_auth に対応できない可能性もあります。\nhttps://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization をご覧ください。",
+       "img-auth-nopathinfo": "URL のパス情報が見つかりません。\nサーバーは、変数 REQUEST_URI または PATH_INFO の一方または両方でパス情報を渡すように構成する必要があります。\nすでに設定済みの場合は、$wgUsePathInfo を有効にすることをお試しください。\nhttps://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization をご覧ください。",
        "img-auth-notindir": "要求されたパスは、設定済みのアップロード先ディレクトリ内にありません。",
        "img-auth-badtitle": "「$1」からは有効なページ名を構築できません。",
        "img-auth-nologinnWL": "ログインしておらず、さらに「$1」はホワイトリストに入っていません。",
        "cachedspecial-refresh-now": "最新版を表示します。",
        "categories": "カテゴリ",
        "categories-submit": "表示",
-       "categoriespagetext": "以ä¸\8bã\81®{{PLURAL:$1|ã\82«ã\83\86ã\82´ã\83ª}}ã\81«ã\81¯ã\83\9aã\83¼ã\82¸ã\81¾ã\81\9fã\81¯ã\83¡ã\83\87ã\82£ã\82¢ã\81\8cã\81\82ã\82\8aã\81¾ã\81\99ã\80\82\n[[Special:UnusedCategories|使ã\82\8fã\82\8cã\81¦ã\81\84ã\81ªã\81\84ã\82«ã\83\86ã\82´ã\83ª]]ã\81¯ã\81\93ã\81\93ã\81«ã\81¯è¡¨ç¤ºã\81\97ã\81¦ã\81\84ã\81¾ã\81\9bã\82\93。\n[[Special:WantedCategories|カテゴリページが存在しないカテゴリ]]も参照してください。",
+       "categoriespagetext": "以ä¸\8bã\81®{{PLURAL:$1|ã\82«ã\83\86ã\82´ã\83ª}}ã\81¯ã\82¦ã\82£ã\82­ä¸\8aã\81«ã\81\82ã\82\8aã\80\81æ\9cªä½¿ç\94¨ã\81§ã\81\82ã\82\8bå ´å\90\88ã\82\82ã\81\82ã\82\8aã\81¾ã\81\99。\n[[Special:WantedCategories|カテゴリページが存在しないカテゴリ]]も参照してください。",
        "categoriesfrom": "最初に表示するカテゴリ:",
        "deletedcontributions": "利用者の削除された投稿",
        "deletedcontributions-title": "利用者の削除された投稿",
index f1a5324..8af374b 100644 (file)
        "headline_tip": "အဆင့် ၂ ခေါင်းစီး",
        "nowiki_sample": "ဖောမတ်မလုပ်ထားသော စာများကို ဤနေရာတွင် ထည့်ရန်",
        "nowiki_tip": "ဝီကီပုံစံ ဖော်မတ်များကို လျစ်လျူရှုရန်",
+       "image_sample": "ဥပမာ.jpg",
        "image_tip": "Embedded ထည့်ထားသော ဖိုင်",
+       "media_sample": "ဥပမာ.ogg",
        "media_tip": "ဖိုင်လင့်",
        "sig_tip": "အချိန်ပါပြသော သင့်လက်မှတ်",
        "hr_tip": "မျဉ်းလဲ (စိစစ်သုံးရန်)",
        "recentchanges-label-plusminus": "စာမျက်နှာ အရွယ်အစားမှာ အောက်ပါ ဘိုက်ပမာဏ ပြောင်းလဲသွားခဲ့သည်",
        "recentchanges-legend-heading": "<strong>အညွှန်း:</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} ([[Special:NewPages|စာမျက်နှာသစ်များ စာရင်း]]ကိုလည်း ကြည့်ရန်)",
+       "recentchanges-legend-plusminus": "(<em>±၁၂၃</em>)",
        "recentchanges-submit": "ပြသရန်",
        "rcfilters-tag-remove": "'$1' ကို ဖယ်ရှားရန်",
        "rcfilters-legend-heading": "<strong>အတိုကောက်များ စာရင်း:</strong>",
index b6558b6..6283aec 100644 (file)
        "confirm-unwatch-top": "Used as confirmation message.",
        "confirm-rollback-button": "Used as Submit button text.\n{{Identical|OK}}",
        "confirm-rollback-top": "Used as confirmation message.",
+       "confirm-mcrundo-title": "Title for the editless undo form.",
+       "mcrundo-changed": "Message displayed when the page has been edited between the loading and submission of an editless undo.",
+       "mcrundo-missingparam": "Error displayed when parameters for action=mcrundo are missing",
+       "mcrundofailed": "Title of the error page when an editless undo fails.",
        "semicolon-separator": "{{optional}}",
        "comma-separator": "{{optional}}\n\nWarning: languages have different usages of punctuation, and sometimes they are swapped (e.g. openining and closing quotation marks, or full stop and colon in Armenian), or change their form (the full stop in Chinese and Japanese, the prefered \"colon\" in Armenian used in fact as the regular full stop, the comma in Arabic, Armenian, and Chinese...)\n\nTheir spacing (before or after) may also vary across languages (for example French requires a non-breaking space, preferably narrow if the browser supports NNBSP, on the inner side of some punctuations like quotation/question/exclamation marks, colon, and semicolons).",
        "colon-separator": "{{optional}}\nChange it only if your language uses another character for ':' or it needs an extra space before the colon.",
index 86d9b67..9546e52 100644 (file)
        "tog-watchdefault": "Добавлять в список наблюдения изменённые мной страницы и описания файлов",
        "tog-watchmoves": "Добавлять в список наблюдения переименованные мной страницы и файлы",
        "tog-watchdeletion": "Добавлять в список наблюдения удалённые мной страницы и файлы",
-       "tog-watchuploads": "Ð\94обавлÑ\8fÑ\82Ñ\8c Ð·Ð°ÐºÐ°Ñ\87анные мною файлы в список наблюдения",
+       "tog-watchuploads": "Ð\94обавлÑ\8fÑ\82Ñ\8c Ð·Ð°Ð³Ñ\80Ñ\83женные мною файлы в список наблюдения",
        "tog-watchrollback": "Добавлять страницы, где я выполнил откат, в мой список наблюдения",
        "tog-minordefault": "По умолчанию помечать правки как малые",
        "tog-previewontop": "Помещать предпросмотр перед окном редактирования",
index e215bd7..2330ae4 100644 (file)
        "rcfilters-invalid-filter": "Яраксыз фильтр",
        "rcfilters-filterlist-title": "Фильтрлар",
        "rcfilters-filterlist-feedbacklink": "Әлеге фильтрлау кораллары турында турында фикер калдырыгыз",
+       "rcfilters-highlightmenu-title": "Төсен сайлагыз",
+       "rcfilters-highlightmenu-help": "Үзлекләрен аеру өчен аның төсен сайлагыз",
        "rcfilters-filtergroup-authorship": "Үзгәртүләрнең авторлыгы",
        "rcfilters-filter-editsbyself-label": "Сезнең үзгәртүләр",
        "rcfilters-filter-editsbyself-description": "Сезнең кертемегез.",
index 8cda4f9..fe064f5 100644 (file)
@@ -687,6 +687,8 @@ CREATE TABLE /*_*/slots (
 
   -- The revision ID of the revision that originated the slot's content.
   -- To find revisions that changed slots, look for slot_origin = slot_revision_id.
+  -- TODO: Is that actually true? Rollback seems to violate it by setting
+  --  slot_origin to an older rev_id. Undeletions could result in the same situation.
   slot_origin bigint unsigned NOT NULL,
 
   PRIMARY KEY ( slot_revision_id, slot_role_id )
index 6ed226c..06eb80e 100644 (file)
                         * @param {string[]} batch
                         */
                        function batchRequest( batch ) {
-                               var reqBase, splits, maxQueryLength, b, bSource, bGroup, bSourceGroup,
+                               var reqBase, splits, maxQueryLength, b, bSource, bGroup,
                                        source, group, i, modules, sourceLoadScript,
                                        currReqBase, currReqBaseLength, moduleMap, currReqModules, l,
                                        lastDotIndex, prefix, suffix, bytesAdded;
                                maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', 2000 );
 
                                // Split module list by source and by group.
-                               splits = {};
+                               splits = Object.create( null );
                                for ( b = 0; b < batch.length; b++ ) {
                                        bSource = registry[ batch[ b ] ].source;
                                        bGroup = registry[ batch[ b ] ].group;
-                                       if ( !hasOwn.call( splits, bSource ) ) {
-                                               splits[ bSource ] = {};
+                                       if ( !splits[ bSource ] ) {
+                                               splits[ bSource ] = Object.create( null );
                                        }
-                                       if ( !hasOwn.call( splits[ bSource ], bGroup ) ) {
+                                       if ( !splits[ bSource ][ bGroup ] ) {
                                                splits[ bSource ][ bGroup ] = [];
                                        }
-                                       bSourceGroup = splits[ bSource ][ bGroup ];
-                                       bSourceGroup.push( batch[ b ] );
+                                       splits[ bSource ][ bGroup ].push( batch[ b ] );
                                }
 
                                for ( source in splits ) {
-
                                        sourceLoadScript = sources[ source ];
 
                                        for ( group in splits[ source ] ) {
                                                // We may need to split up the request to honor the query string length limit,
                                                // so build it piece by piece.
                                                l = currReqBaseLength;
-                                               moduleMap = {}; // { prefix: [ suffixes ] }
+                                               moduleMap = Object.create( null ); // { prefix: [ suffixes ] }
                                                currReqModules = [];
 
                                                for ( i = 0; i < modules.length; i++ ) {
                                                        // If lastDotIndex is -1, substr() returns an empty string
                                                        prefix = modules[ i ].substr( 0, lastDotIndex );
                                                        suffix = modules[ i ].slice( lastDotIndex + 1 );
-                                                       bytesAdded = hasOwn.call( moduleMap, prefix ) ?
+                                                       bytesAdded = moduleMap[ prefix ] ?
                                                                suffix.length + 3 : // '%2C'.length == 3
                                                                modules[ i ].length + 3; // '%7C'.length == 3
 
                                                                doRequest();
                                                                // .. and start again.
                                                                l = currReqBaseLength;
-                                                               moduleMap = {};
+                                                               moduleMap = Object.create( null );
                                                                currReqModules = [];
 
                                                                mw.track( 'resourceloader.splitRequest', { maxQueryLength: maxQueryLength } );
                                                        }
-                                                       if ( !hasOwn.call( moduleMap, prefix ) ) {
+                                                       if ( !moduleMap[ prefix ] ) {
                                                                moduleMap[ prefix ] = [];
                                                        }
                                                        l += bytesAdded;
                                                module: {
                                                        exports: {}
                                                },
-                                               version: version !== undefined ? String( version ) : '',
+                                               version: String( version || '' ),
                                                dependencies: deps || [],
                                                group: typeof group === 'string' ? group : null,
                                                source: typeof source === 'string' ? source : 'local',
                                /**
                                 * Change the state of one or more modules.
                                 *
-                                * @param {Object|string} modules Object of module name/state pairs
+                                * @param {Object} modules Object of module name/state pairs
                                 */
                                state: function ( modules ) {
                                        var module, state;
index cbae4c7..492d00c 100644 (file)
@@ -173,51 +173,84 @@ class McrReadNewRevisionStoreDbTest extends RevisionStoreDbTestBase {
        }
 
        public function provideGetSlotsQueryInfo() {
-               yield [
+               yield 'no options' => [
                        [],
+                       [
+                               'tables' => [
+                                       'slots'
+                               ],
+                               'fields' => [
+                                       'slot_revision_id',
+                                       'slot_content_id',
+                                       'slot_origin',
+                                       'slot_role_id',
+                               ],
+                               'joins' => [],
+                       ]
+               ];
+               yield 'role option' => [
+                       [ 'role' ],
                        [
                                'tables' => [
                                        'slots',
                                        'slot_roles',
                                ],
-                               'fields' => array_merge(
-                                       [
-                                               'slot_revision_id',
-                                               'slot_content_id',
-                                               'slot_origin',
-                                               'role_name',
-                                       ]
-                               ),
+                               'fields' => [
+                                       'slot_revision_id',
+                                       'slot_content_id',
+                                       'slot_origin',
+                                       'slot_role_id',
+                                       'role_name',
+                               ],
                                'joins' => [
-                                       'slot_roles' => [ 'INNER JOIN', [ 'slot_role_id = role_id' ] ],
+                                       'slot_roles' => [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ],
                                ],
                        ]
                ];
-               yield [
+               yield 'content option' => [
                        [ 'content' ],
                        [
                                'tables' => [
                                        'slots',
-                                       'slot_roles',
+                                       'content',
+                               ],
+                               'fields' => [
+                                       'slot_revision_id',
+                                       'slot_content_id',
+                                       'slot_origin',
+                                       'slot_role_id',
+                                       'content_size',
+                                       'content_sha1',
+                                       'content_address',
+                                       'content_model',
+                               ],
+                               'joins' => [
+                                       'content' => [ 'INNER JOIN', [ 'slot_content_id = content_id' ] ],
+                               ],
+                       ]
+               ];
+               yield 'content and model options' => [
+                       [ 'content', 'model' ],
+                       [
+                               'tables' => [
+                                       'slots',
                                        'content',
                                        'content_models',
                                ],
-                               'fields' => array_merge(
-                                       [
-                                               'slot_revision_id',
-                                               'slot_content_id',
-                                               'slot_origin',
-                                               'role_name',
-                                               'content_size',
-                                               'content_sha1',
-                                               'content_address',
-                                               'model_name',
-                                       ]
-                               ),
+                               'fields' => [
+                                       'slot_revision_id',
+                                       'slot_content_id',
+                                       'slot_origin',
+                                       'slot_role_id',
+                                       'content_size',
+                                       'content_sha1',
+                                       'content_address',
+                                       'content_model',
+                                       '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' ] ],
+                                       'content_models' => [ 'LEFT JOIN', [ 'content_model = model_id' ] ],
                                ],
                        ]
                ];
index 649e692..af19f72 100644 (file)
@@ -190,51 +190,84 @@ class McrRevisionStoreDbTest extends RevisionStoreDbTestBase {
        }
 
        public function provideGetSlotsQueryInfo() {
-               yield [
+               yield 'no options' => [
                        [],
+                       [
+                               'tables' => [
+                                       'slots'
+                               ],
+                               'fields' => [
+                                       'slot_revision_id',
+                                       'slot_content_id',
+                                       'slot_origin',
+                                       'slot_role_id',
+                               ],
+                               'joins' => [],
+                       ]
+               ];
+               yield 'role option' => [
+                       [ 'role' ],
                        [
                                'tables' => [
                                        'slots',
                                        'slot_roles',
                                ],
-                               'fields' => array_merge(
-                                       [
-                                               'slot_revision_id',
-                                               'slot_content_id',
-                                               'slot_origin',
-                                               'role_name',
-                                       ]
-                               ),
+                               'fields' => [
+                                       'slot_revision_id',
+                                       'slot_content_id',
+                                       'slot_origin',
+                                       'slot_role_id',
+                                       'role_name',
+                               ],
                                'joins' => [
-                                       'slot_roles' => [ 'INNER JOIN', [ 'slot_role_id = role_id' ] ],
+                                       'slot_roles' => [ 'LEFT JOIN', [ 'slot_role_id = role_id' ] ],
                                ],
                        ]
                ];
-               yield [
+               yield 'content option' => [
                        [ 'content' ],
                        [
                                'tables' => [
                                        'slots',
-                                       'slot_roles',
+                                       'content',
+                               ],
+                               'fields' => [
+                                       'slot_revision_id',
+                                       'slot_content_id',
+                                       'slot_origin',
+                                       'slot_role_id',
+                                       'content_size',
+                                       'content_sha1',
+                                       'content_address',
+                                       'content_model',
+                               ],
+                               'joins' => [
+                                       'content' => [ 'INNER JOIN', [ 'slot_content_id = content_id' ] ],
+                               ],
+                       ]
+               ];
+               yield 'content and model options' => [
+                       [ 'content', 'model' ],
+                       [
+                               'tables' => [
+                                       'slots',
                                        'content',
                                        'content_models',
                                ],
-                               'fields' => array_merge(
-                                       [
-                                               'slot_revision_id',
-                                               'slot_content_id',
-                                               'slot_origin',
-                                               'role_name',
-                                               'content_size',
-                                               'content_sha1',
-                                               'content_address',
-                                               'model_name',
-                                       ]
-                               ),
+                               'fields' => [
+                                       'slot_revision_id',
+                                       'slot_content_id',
+                                       'slot_origin',
+                                       'slot_role_id',
+                                       'content_size',
+                                       'content_sha1',
+                                       'content_address',
+                                       'content_model',
+                                       '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' ] ],
+                                       'content_models' => [ 'LEFT JOIN', [ 'content_model = model_id' ] ],
                                ],
                        ]
                ];
index 40f6807..3336235 100644 (file)
@@ -86,14 +86,17 @@ class DifferenceEngineTest extends MediaWikiTestCase {
        public function testLoadRevisionData() {
                $cases = $this->getLoadRevisionDataCases();
 
-               foreach ( $cases as $case ) {
-                       list( $expectedOld, $expectedNew, $old, $new, $message ) = $case;
+               foreach ( $cases as $testName => $case ) {
+                       list( $expectedOld, $expectedNew, $expectedRet, $old, $new ) = $case;
 
                        $diffEngine = new DifferenceEngine( $this->context, $old, $new, 2, true, false );
-                       $diffEngine->loadRevisionData();
+                       $ret = $diffEngine->loadRevisionData();
+                       $ret2 = $diffEngine->loadRevisionData();
 
-                       $this->assertEquals( $diffEngine->getOldid(), $expectedOld, $message );
-                       $this->assertEquals( $diffEngine->getNewid(), $expectedNew, $message );
+                       $this->assertEquals( $expectedOld, $diffEngine->getOldid(), $testName );
+                       $this->assertEquals( $expectedNew, $diffEngine->getNewid(), $testName );
+                       $this->assertEquals( $expectedRet, $ret, $testName );
+                       $this->assertEquals( $expectedRet, $ret2, $testName );
                }
        }
 
@@ -101,10 +104,12 @@ class DifferenceEngineTest extends MediaWikiTestCase {
                $revs = self::$revisions;
 
                return [
-                       [ $revs[2], $revs[3], $revs[3], 'prev', 'diff=prev' ],
-                       [ $revs[2], $revs[3], $revs[2], 'next', 'diff=next' ],
-                       [ $revs[1], $revs[3], $revs[1], $revs[3], 'diff=' . $revs[3] ],
-                       [ $revs[1], $revs[3], $revs[1], 0, 'diff=0' ]
+                       'diff=prev' => [ $revs[2], $revs[3], true, $revs[3], 'prev' ],
+                       'diff=next' => [ $revs[2], $revs[3], true, $revs[2], 'next' ],
+                       'diff=' . $revs[3] => [ $revs[1], $revs[3], true, $revs[1], $revs[3] ],
+                       'diff=0' => [ $revs[1], $revs[3], true, $revs[1], 0 ],
+                       'diff=prev&oldid=<first>' => [ false, $revs[0], true, $revs[0], 'prev' ],
+                       'invalid' => [ 123456789, $revs[1], false, 123456789, $revs[1] ],
                ];
        }