the parser allowing them to insert malicious attributes. Disabled by default,
you can configure this via $wgCSPHeader and $wgCSPReportOnlyHeader.
* New configuration variable has been added: $wgCookieSetOnIpBlock.
- This determines whether to set a cookie when an IP user is blocked. Doing so means
- that a blocked user, even after moving to a new IP address, will still be blocked.
+ This determines whether to set a cookie when an IP user is blocked. Doing so
+ means that a blocked user, even after moving to a new IP address, will still
+ be blocked.
* The archive table's ar_rev_id field is now unique.
* Special:BotPasswords now requires reauthentication.
* Added 'ApiParseMakeOutputPage' hook.
* (T174313) Added checkbox on Special:ListUsers to display only users in
temporary user groups.
-* (T152462) A cookie can now be set when an IP user is blocked to track that user if
- they move to a new IP address. This is disabled by default.
+* (T152462) A cookie can now be set when an IP user is blocked to track that
+ user if they move to a new IP address. This is disabled by default.
* (T194950) Added 'ApiMaxLagInfo' hook.
* SpecialPage::checkLoginSecurityLevel() will now preserve POST data when
reauthenticating.
* FormSpecialPage::execute() will now call checkLoginSecurityLevel() if
getLoginSecurityLevel() returns non-false.
* The 'ImageBeforeProduceHTML' hook is now passed three new parameters, $parser,
- &$query and &$widthOption, allowing extensions even finer control over the resulting
- HTML code.
+ &$query and &$widthOption, allowing extensions even finer control over the
+ resulting HTML code.
=== External library changes in 1.32 ===
* …
"selectorWithVariant": {
"type": "string"
},
+ "useDataURI": {
+ "type": "boolean"
+ },
"variants": {
"type": "object"
},
"selectorWithVariant": {
"type": "string"
},
+ "useDataURI": {
+ "type": "boolean"
+ },
"variants": {
"type": "object"
},
$additionalSelfUrls = $this->getAdditionalSelfUrls();
$additionalSelfUrlsScript = $this->getAdditionalSelfUrlsScript();
- $nonceSrc = "'nonce-" . $this->nonce . "'";
// If no default-src is sent at all, it
// seems browsers (or at least some), interpret
$cssSrc = false;
$imgSrc = false;
$scriptSrc = [ "'unsafe-eval'", "'self'" ];
- if ( $mode !== self::FULL_MODE_RESTRICTED ) {
+ if (
+ $mode !== self::FULL_MODE_RESTRICTED &&
+ ( !isset( $policyConfig['useNonces'] ) || $policyConfig['useNonces'] )
+ ) {
+ $nonceSrc = "'nonce-" . $this->nonce . "'";
$scriptSrc[] = $nonceSrc;
}
$scriptSrc = array_merge( $scriptSrc, $additionalSelfUrlsScript );
}
/**
- * Is CSP currently enabled (i.e. Should we set nonce attribute)
+ * Should we set nonce attribute
*
* @param Config $config Configuration object
* @return bool
*/
- public static function isEnabled( Config $config ) {
- return $config->get( 'CSPHeader' ) !== false
- || $config->get( 'CSPReportOnlyHeader' ) !== false;
+ public static function isNonceRequired( Config $config ) {
+ $configs = [
+ $config->get( 'CSPHeader' ),
+ $config->get( 'CSPReportOnlyHeader' )
+ ];
+ foreach ( $configs as $headerConfig ) {
+ if (
+ $headerConfig === true ||
+ ( is_array( $headerConfig ) &&
+ !isset( $headerConfig['useNonces'] ) ) ||
+ ( is_array( $headerConfig ) &&
+ isset( $headerConfig['useNonces'] ) &&
+ $headerConfig['useNonces'] )
+ ) {
+ return true;
+ }
+ }
+ return false;
}
}
* $wgCrossSiteAJAXdomains as an allowed load sources.
* 'unsafeFallback' Add unsafe-inline as a script source, as a fallback for
* browsers that do not understand nonce-sources [default on].
+ * 'useNonces' Require nonces on all inline scripts. If disabled and 'unsafeFallback'
+ * is on, then all inline scripts will be allowed [default true].
* 'script-src' Array of additional places that are allowed to have JS be loaded from.
* 'report-uri' true to use MW api [default], false to disable, string for alternate uri
* @warning May cause slowness on windows due to slow random number generator.
if ( $nonce !== null ) {
$attrs['nonce'] = $nonce;
} else {
- if ( ContentSecurityPolicy::isEnabled( RequestContext::getMain()->getConfig() ) ) {
+ if ( ContentSecurityPolicy::isNonceRequired( RequestContext::getMain()->getConfig() ) ) {
wfWarn( "no nonce set on script. CSP will break it" );
}
}
if ( $nonce !== null ) {
$attrs['nonce'] = $nonce;
} else {
- if ( ContentSecurityPolicy::isEnabled( RequestContext::getMain()->getConfig() ) ) {
+ if ( ContentSecurityPolicy::isNonceRequired( RequestContext::getMain()->getConfig() ) ) {
wfWarn( "no nonce set on script. CSP will break it" );
}
}
* @since 1.32
*/
public function getCSPNonce() {
- if ( !ContentSecurityPolicy::isEnabled( $this->getConfig() ) ) {
+ if ( !ContentSecurityPolicy::isNonceRequired( $this->getConfig() ) ) {
return false;
}
if ( $this->CSPNonce === null ) {
// Defend against mistakes caused by differences with the
// signature of WikiPage::doEditContent.
Assert::parameterType( 'integer', $flags, '$flags' );
- Assert::parameterType( 'CommentStoreComment', $summary, '$summary' );
if ( $this->wasCommitted() ) {
throw new RuntimeException( 'saveRevision() has already been called on this PageUpdater!' );
return null;
}
- // Fetch the actual revision row, without locking all extra tables.
- $oldRevision = $this->loadRevisionFromId( $dbw, $pageLatest );
+ // Fetch the actual revision row from master, without locking all extra tables.
+ $oldRevision = $this->loadRevisionFromConds(
+ $dbw,
+ [ 'rev_id' => intval( $pageLatest ) ],
+ self::READ_LATEST,
+ $title
+ );
// Construct the new revision
$timestamp = wfTimestampNow(); // TODO: use a callback, so we can override it for testing.
"apihelp-compare-param-fromtitle": "比較する1つ目のページ名。",
"apihelp-compare-param-fromid": "比較する1つ目のページID。",
"apihelp-compare-param-fromrev": "比較する1つ目の版。",
+ "apihelp-compare-param-fromtext": "<var>fromtitle</var>, <var>fromid</var> or <var>fromrev</var> で指定された版の内容の代わりに、このテキストを使用します。",
+ "apihelp-compare-param-fromsection": "'from' の内容のうち指定された節のみを使用します。",
"apihelp-compare-param-frompst": "<var>fromtext</var>に保存前変換を行います。",
"apihelp-compare-param-fromcontentmodel": "<var>fromtext</var>のコンテンツモデル。指定されていない場合は、他のパラメータに基づいて推測されます。",
"apihelp-compare-param-totitle": "比較する2つ目のページ名。",
"apihelp-compare-param-toid": "比較する2つ目のページID。",
"apihelp-compare-param-torev": "比較する2つ目の版。",
+ "apihelp-compare-param-tosection": "'to' の内容のうち指定された節のみを使用します。",
"apihelp-compare-param-topst": "<var>totext</var>に保存前変換を行います。",
+ "apihelp-compare-param-tocontentmodel": "<var>totext</var> のコンテンツモデル。指定されていない場合は、他のパラメータに基づいて推測されます。",
"apihelp-compare-param-prop": "どの情報を取得するか:",
"apihelp-compare-paramvalue-prop-diff": "差分HTML。",
"apihelp-compare-paramvalue-prop-diffsize": "差分HTMLのサイズ (バイト数)。",
* @return bool
*/
public static function useFileCache( IContextSource $context, $mode = self::MODE_NORMAL ) {
+ global $wgContLang;
$config = MediaWikiServices::getInstance()->getMainConfig();
if ( !$config->get( 'UseFileCache' ) && $mode !== self::MODE_REBUILD ) {
$ulang = $context->getLanguage();
// Check that there are no other sources of variation
- if ( $user->getId() || $ulang->getCode() !== $config->get( 'LanguageCode' ) ) {
+ if ( $user->getId() || !$ulang->equals( $wgContLang ) ) {
return false;
}
* @since 1.27
*/
-use Wikimedia\Assert\Assert;
-
class CategoryMembershipChange {
const CATEGORY_ADDITION = 1;
*
* @throws MWException
*/
- public function overrideNewForCategorizationCallback( $callback ) {
+ public function overrideNewForCategorizationCallback( callable $callback ) {
if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
throw new MWException( 'Cannot override newForCategorization callback in operation.' );
}
- Assert::parameterType( 'callable', $callback, '$callback' );
$this->newForCategorizationCallback = $callback;
}
$this->output( "done.\n" );
}
}
+
+ /**
+ * Populates the MCR content tables
+ * @since 1.32
+ */
+ protected function populateContentTables() {
+ global $wgMultiContentRevisionSchemaMigrationStage;
+ if ( ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW ) &&
+ !$this->updateRowExists( 'PopulateContentTables' )
+ ) {
+ $this->output(
+ "Migrating revision data to the MCR 'slot' and 'content' tables, printing progress markers.\n" .
+ "For large databases, you may want to hit Ctrl-C and do this manually with\n" .
+ "maintenance/populateContentTables.php.\n"
+ );
+ $task = $this->maintenance->runChild(
+ PopulateContentTables::class, 'populateContentTables.php'
+ );
+ $ok = $task->execute();
+ $this->output( $ok ? "done.\n" : "errors were encountered.\n" );
+ if ( $ok ) {
+ $this->insertUpdateRow( 'PopulateContentTables' );
+ }
+ }
+ }
}
[ 'runMaintenance', DeduplicateArchiveRevId::class, 'maintenance/deduplicateArchiveRevId.php' ],
[ 'addField', 'change_tag', 'ct_tag_id', 'patch-change_tag-tag_id.sql' ],
[ 'addIndex', 'archive', 'ar_revid_uniq', 'patch-archive-ar_rev_id-unique.sql' ],
+ [ 'populateContentTables' ],
];
}
[ 'runMaintenance', DeduplicateArchiveRevId::class, 'maintenance/deduplicateArchiveRevId.php' ],
[ 'addField', 'change_tag', 'ct_tag_id', 'patch-change_tag-tag_id.sql' ],
[ 'addIndex', 'archive', 'ar_revid_uniq', 'patch-archive-ar_rev_id-unique.sql' ],
+ [ 'populateContentTables' ],
];
}
[ 'runMaintenance', DeduplicateArchiveRevId::class, 'maintenance/deduplicateArchiveRevId.php' ],
[ 'addField', 'change_tag', 'ct_tag_id', 'patch-change_tag-tag_id.sql' ],
[ 'addIndex', 'archive', 'ar_revid_uniq', 'patch-archive-ar_rev_id-unique.sql' ],
+ [ 'populateContentTables' ],
// KEEP THIS AT THE BOTTOM!!
[ 'doRebuildDuplicateFunction' ],
],
[ 'addPgIndex', 'archive', 'ar_revid_uniq', '(ar_rev_id)', 'unique' ],
[ 'dropPgIndex', 'archive', 'ar_revid' ], // Probably doesn't exist, but do it anyway.
+ [ 'populateContentTables' ],
];
}
[ 'runMaintenance', DeduplicateArchiveRevId::class, 'maintenance/deduplicateArchiveRevId.php' ],
[ 'addField', 'change_tag', 'ct_tag_id', 'patch-change_tag-tag_id.sql' ],
[ 'addIndex', 'archive', 'ar_revid_uniq', 'patch-archive-ar_rev_id-unique.sql' ],
+ [ 'populateContentTables' ],
];
}
*/
use MediaWiki\MediaWikiServices;
-use Wikimedia\Rdbms\ResultWrapper;
+use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Storage\RevisionStore;
+use MediaWiki\Storage\SqlBlobStore;
+use Wikimedia\Assert\Assert;
+use Wikimedia\Rdbms\IResultWrapper;
use Wikimedia\Rdbms\IDatabase;
/**
$this->config = $config;
}
+ /**
+ * @return RevisionStore
+ */
+ private function getRevisionStore() {
+ // TODO: Refactor: delete()/undelete() should live in a PageStore service;
+ // Methods in PageArchive and RevisionStore that deal with archive revisions
+ // should move into an ArchiveStore service (but could still be implemented
+ // together with RevisionStore).
+ return MediaWikiServices::getInstance()->getRevisionStore();
+ }
+
public function doesWrites() {
return true;
}
* wrapper with (ar_namespace, ar_title, count) fields, ordered by page
* namespace/title.
*
- * @return ResultWrapper
+ * @deprecated since 1.32.
+ *
+ * @return IResultWrapper
*/
public static function listAllPages() {
+ wfDeprecated( __METHOD__, '1.32' );
+
$dbr = wfGetDB( DB_REPLICA );
return self::listPages( $dbr, '' );
* Returns result wrapper with (ar_namespace, ar_title, count) fields.
*
* @param string $term Search term
- * @return ResultWrapper
+ * @return IResultWrapper
*/
public static function listPagesBySearch( $term ) {
$title = Title::newFromText( $term );
* Returns result wrapper with (ar_namespace, ar_title, count) fields.
*
* @param string $prefix Title prefix
- * @return ResultWrapper
+ * @return IResultWrapper
*/
public static function listPagesByPrefix( $prefix ) {
$dbr = wfGetDB( DB_REPLICA );
/**
* @param IDatabase $dbr
* @param string|array $condition
- * @return bool|ResultWrapper
+ * @return bool|IResultWrapper
*/
protected static function listPages( $dbr, $condition ) {
return $dbr->select(
* List the revisions of the given page. Returns result wrapper with
* various archive table fields.
*
- * @return ResultWrapper
+ * @return IResultWrapper
*/
public function listRevisions() {
- $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
+ $revisionStore = $this->getRevisionStore();
$queryInfo = $revisionStore->getArchiveQueryInfo();
$conds = [
'ar_namespace' => $this->title->getNamespace(),
'ar_title' => $this->title->getDBkey(),
];
- $options = [ 'ORDER BY' => 'ar_timestamp DESC' ];
+
+ // NOTE: ordering by ar_timestamp and ar_id, to remove ambiguity.
+ // XXX: Ideally, we would be ordering by ar_timestamp and ar_rev_id, but since we
+ // don't have an index on ar_rev_id, that causes a file sort.
+ $options = [ 'ORDER BY' => 'ar_timestamp DESC, ar_id DESC' ];
ChangeTags::modifyDisplayQuery(
$queryInfo['tables'],
* Returns a result wrapper with various filearchive fields, or null
* if not a file page.
*
- * @return ResultWrapper
+ * @return IResultWrapper
* @todo Does this belong in Image for fuller encapsulation?
*/
public function listFiles() {
/**
* Return a Revision object containing data for the deleted revision.
- * Note that the result *may* or *may not* have a null page ID.
+ *
+ * @deprecated since 1.32, use getArchivedRevision() instead.
*
* @param string $timestamp
* @return Revision|null
*/
public function getRevision( $timestamp ) {
$dbr = wfGetDB( DB_REPLICA );
- $arQuery = Revision::getArchiveQueryInfo();
+ $rec = $this->getRevisionByConditions(
+ [ 'ar_timestamp' => $dbr->timestamp( $timestamp ) ]
+ );
+ return $rec ? new Revision( $rec ) : null;
+ }
+
+ /**
+ * Return the archived revision with the given ID.
+ *
+ * @param int $revId
+ * @return Revision|null
+ */
+ public function getArchivedRevision( $revId ) {
+ // Protect against code switching from getRevision() passing in a timestamp.
+ Assert::parameterType( 'integer', $revId, '$revId' );
+
+ $rec = $this->getRevisionByConditions( [ 'ar_rev_id' => $revId ] );
+ return $rec ? new Revision( $rec ) : null;
+ }
+
+ /**
+ * @param array $conditions
+ * @param array $options
+ *
+ * @return RevisionRecord|null
+ */
+ private function getRevisionByConditions( array $conditions, array $options = [] ) {
+ $dbr = wfGetDB( DB_REPLICA );
+ $arQuery = $this->getRevisionStore()->getArchiveQueryInfo();
+
+ $conditions = $conditions + [
+ 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey(),
+ ];
$row = $dbr->selectRow(
$arQuery['tables'],
$arQuery['fields'],
- [
- 'ar_namespace' => $this->title->getNamespace(),
- 'ar_title' => $this->title->getDBkey(),
- 'ar_timestamp' => $dbr->timestamp( $timestamp )
- ],
+ $conditions,
__METHOD__,
- [],
+ $options,
$arQuery['joins']
);
if ( $row ) {
- return Revision::newFromArchiveRow( $row, [ 'title' => $this->title ] );
+ return $this->getRevisionStore()->newRevisionFromArchiveRow( $row, 0, $this->title );
}
return null;
// Check the previous deleted revision...
$row = $dbr->selectRow( 'archive',
- 'ar_timestamp',
+ [ 'ar_id', 'ar_timestamp' ],
[ 'ar_namespace' => $this->title->getNamespace(),
'ar_title' => $this->title->getDBkey(),
'ar_timestamp < ' .
'ORDER BY' => 'ar_timestamp DESC',
'LIMIT' => 1 ] );
$prevDeleted = $row ? wfTimestamp( TS_MW, $row->ar_timestamp ) : false;
+ $prevDeletedId = $row ? intval( $row->ar_rev_id ) : null;
$row = $dbr->selectRow( [ 'page', 'revision' ],
[ 'rev_id', 'rev_timestamp' ],
if ( $prevLive && $prevLive > $prevDeleted ) {
// Most prior revision was live
- return Revision::newFromId( $prevLiveId );
+ $rec = $this->getRevisionStore()->getRevisionById( $prevLiveId );
+ $rec = $rec ? new Revision( $rec ) : null;
} elseif ( $prevDeleted ) {
// Most prior revision was deleted
- return $this->getRevision( $prevDeleted );
+ $rec = $this->getArchivedRevision( $prevDeletedId );
+ } else {
+ $rec = null;
}
- // No prior revision on this page.
- return null;
+ return $rec;
}
/**
- * Get the text from an archive row containing ar_text_id
+ * Get the text from an archive row containing ar_text_id.
+ *
+ * @deprecated since 1.32. In the MCR schema, ar_text_id no longer exists.
+ * Calling code should switch to getArchiveRevision().
+ *
+ * @todo remove in 1.33
*
- * @deprecated since 1.31
* @param object $row Database row
* @return string
*/
public function getTextFromRow( $row ) {
- $dbr = wfGetDB( DB_REPLICA );
- $text = $dbr->selectRow( 'text',
- [ 'old_text', 'old_flags' ],
- [ 'old_id' => $row->ar_text_id ],
- __METHOD__ );
+ wfDeprecated( __METHOD__, '1.32' );
+
+ if ( empty( $row->ar_text_id ) ) {
+ throw new InvalidArgumentException( '$row->ar_text_id must be set and not empty!' );
+ }
+
+ $address = SqlBlobStore::makeAddressFromTextId( $row->ar_text_id );
+ $blobStore = MediaWikiServices::getInstance()->getBlobStore();
- return Revision::getRevisionText( $text );
+ return $blobStore->getBlob( $address );
}
/**
*
* If there are no archived revisions for the page, returns NULL.
*
+ * @note this bypasses any audience checks.
+ *
+ * @deprecated since 1.32. For compatibility with the MCR schema,
+ * calling code should switch to getLastRevisionId() and getArchiveRevision().
+ *
+ * @todo remove in 1.33
+ *
* @return string|null
*/
public function getLastRevisionText() {
+ wfDeprecated( __METHOD__, '1.32' );
+
+ $revId = $this->getLastRevisionId();
+
+ if ( $revId ) {
+ $rev = $this->getArchivedRevision( $revId );
+ $content = $rev->getContent( RevisionRecord::RAW );
+ return $content->serialize();
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the ID of the latest deleted revision.
+ *
+ * @return int|false The revision's ID, or false if there is no deleted revision.
+ */
+ public function getLastRevisionId() {
$dbr = wfGetDB( DB_REPLICA );
- $row = $dbr->selectRow(
- [ 'archive', 'text' ],
- [ 'old_text', 'old_flags' ],
+ $revId = $dbr->selectField(
+ 'archive',
+ 'ar_rev_id',
[ 'ar_namespace' => $this->title->getNamespace(),
'ar_title' => $this->title->getDBkey() ],
__METHOD__,
- [ 'ORDER BY' => 'ar_timestamp DESC, ar_id DESC' ],
- [ 'text' => [ 'JOIN', 'old_id = ar_text_id' ] ]
+ [ 'ORDER BY' => 'ar_timestamp DESC, ar_id DESC' ]
);
- if ( $row ) {
- return Revision::getRevisionText( $row );
- }
-
- return null;
+ return $revId ? intval( $revId ) : false;
}
/**
* Quick check if any archived revisions are present for the page.
+ * This says nothing about whether the page currently exists in the page table or not.
*
* @return bool
*/
public function isDeleted() {
$dbr = wfGetDB( DB_REPLICA );
- $n = $dbr->selectField( 'archive', 'COUNT(ar_title)',
+ $row = $dbr->selectRow(
+ [ 'archive' ],
+ '1', // We don't care about the value. Allow the database to optimize.
[ 'ar_namespace' => $this->title->getNamespace(),
'ar_title' => $this->title->getDBkey() ],
__METHOD__
);
- return ( $n > 0 );
+ return (bool)$row;
}
/**
$oldWhere['ar_timestamp'] = array_map( [ &$dbw, 'timestamp' ], $timestamps );
}
- $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
+ $revisionStore = $this->getRevisionStore();
$queryInfo = $revisionStore->getArchiveQueryInfo();
$queryInfo['tables'][] = 'revision';
$queryInfo['fields'][] = 'rev_id';
if ( $latestRestorableRow !== null ) {
$oldPageId = (int)$latestRestorableRow->ar_page_id; // pass this to ArticleUndelete hook
- // grab the content to check consistency with global state before restoring the page.
- $revision = Revision::newFromArchiveRow( $latestRestorableRow,
- [
- 'title' => $article->getTitle(), // used to derive default content model
- ]
+ // Grab the content to check consistency with global state before restoring the page.
+ // XXX: The only current use case is Wikibase, which tries to enforce uniqueness of
+ // certain things across all pages. There may be a better way to do that.
+ $revision = $revisionStore->newRevisionFromArchiveRow(
+ $latestRestorableRow,
+ 0,
+ $this->title
);
- $user = User::newFromName( $revision->getUserText( Revision::RAW ), false );
- $content = $revision->getContent( Revision::RAW );
- // NOTE: article ID may not be known yet. prepareSave() should not modify the database.
- $status = $content->prepareSave( $article, 0, -1, $user );
- if ( !$status->isOK() ) {
- $dbw->endAtomic( __METHOD__ );
+ // TODO: use User::newFromUserIdentity from If610c68f4912e
+ // TODO: The User isn't used for anything in prepareSave()! We should drop it.
+ $user = User::newFromName( $revision->getUser( RevisionRecord::RAW )->getName(), false );
- return $status;
+ foreach ( $revision->getSlotRoles() as $role ) {
+ $content = $revision->getContent( $role, RevisionRecord::RAW );
+
+ // NOTE: article ID may not be known yet. prepareSave() should not modify the database.
+ $status = $content->prepareSave( $article, 0, -1, $user );
+ if ( !$status->isOK() ) {
+ $dbw->endAtomic( __METHOD__ );
+
+ return $status;
+ }
}
}
$newid = false; // newly created page ID
$restored = 0; // number of revisions restored
- /** @var Revision $revision */
+ /** @var RevisionRecord|null $revision */
$revision = null;
$restoredPages = [];
// If there are no restorable revisions, we can skip most of the steps.
if ( $makepage ) {
// Check the state of the newest to-be version...
if ( !$unsuppress
- && ( $latestRestorableRow->ar_deleted & Revision::DELETED_TEXT )
+ && ( $latestRestorableRow->ar_deleted & RevisionRecord::DELETED_TEXT )
) {
$dbw->endAtomic( __METHOD__ );
if ( $latestRestorableRow->ar_timestamp > $previousTimestamp ) {
// Check the state of the newest to-be version...
if ( !$unsuppress
- && ( $latestRestorableRow->ar_deleted & Revision::DELETED_TEXT )
+ && ( $latestRestorableRow->ar_deleted & RevisionRecord::DELETED_TEXT )
) {
$dbw->endAtomic( __METHOD__ );
}
// Insert one revision at a time...maintaining deletion status
// unless we are specifically removing all restrictions...
- $revision = Revision::newFromArchiveRow( $row,
+ $revision = $revisionStore->newRevisionFromArchiveRow(
+ $row,
+ 0,
+ $this->title,
[
'page' => $pageId,
- 'title' => $this->title,
'deleted' => $unsuppress ? 0 : $row->ar_deleted
- ] );
+ ]
+ );
// This will also copy the revision to ip_changes if it was an IP edit.
- $revision->insertOn( $dbw );
+ $revisionStore->insertRevisionOn( $revision, $dbw );
$restored++;
+ $legacyRevision = new Revision( $revision );
Hooks::run( 'ArticleRevisionUndeleted',
- [ &$this->title, $revision, $row->ar_page_id ] );
+ [ &$this->title, $legacyRevision, $row->ar_page_id ] );
$restoredPages[$row->ar_page_id] = true;
}
if ( $restored ) {
$created = (bool)$newid;
// Attach the latest revision to the page...
- $wasnew = $article->updateIfNewerOn( $dbw, $revision );
+ // XXX: updateRevisionOn should probably move into a PageStore service.
+ $wasnew = $article->updateIfNewerOn( $dbw, $legacyRevision );
if ( $created || $wasnew ) {
// Update site stats, link tables, etc
+ // TODO: use DerivedPageDataUpdater from If610c68f4912e!
$article->doEditUpdates(
- $revision,
- User::newFromName( $revision->getUserText( Revision::RAW ), false ),
+ $legacyRevision,
+ User::newFromName( $revision->getUser( RevisionRecord::RAW )->getName(), false ),
[
'created' => $created,
'oldcountable' => $oldcountable,
$a = [];
if ( isset( $resetRoutes['username'] ) && $resetRoutes['username'] ) {
$a['Username'] = [
- 'type' => 'text',
+ 'type' => 'user',
'label-message' => 'passwordreset-username',
];
# Add 543 years to the Gregorian calendar
# Months and days are identical
$gy_offset = $gy + 543;
+ # fix for dates between 1912 and 1941
+ # https://en.wikipedia.org/?oldid=836596673#New_year
+ if ( $gy >= 1912 && $gy <= 1940 ) {
+ if ( $gm <= 3 ) {
+ $gy_offset--;
+ }
+ $gm = ( $gm - 3 ) % 12;
+ }
} elseif ( ( !strcmp( $cName, 'minguo' ) ) || !strcmp( $cName, 'juche' ) ) {
# Minguo dates
# Deduct 1911 years from the Gregorian calendar
* @return bool
*/
public function equals( Language $lang ) {
- return $lang->getCode() === $this->mCode;
+ return $lang === $this || $lang->getCode() === $this->mCode;
}
/**
"savechanges": "Gem ændringer",
"publishpage": "Offentliggør side",
"publishchanges": "Offentliggør ændringer",
+ "savearticle-start": "Gem side...",
+ "savechanges-start": "Gem ændringer...",
+ "publishpage-start": "Offentliggør side...",
+ "publishchanges-start": "Offentliggør ændringer...",
"preview": "Forhåndsvisning",
"showpreview": "Forhåndsvisning",
"showdiff": "Vis ændringer",
"rcfilters-other-review-tools": "Andre gennemgangsværktøjer",
"rcfilters-group-results-by-page": "Grupper resultater efter side",
"rcfilters-activefilters": "Aktive filtre",
+ "rcfilters-activefilters-hide": "Skjul",
+ "rcfilters-activefilters-show": "Vis",
"rcfilters-advancedfilters": "Avancerede filtre",
- "rcfilters-limit-title": "Ændringer som skal vises",
+ "rcfilters-limit-title": "Antal resultater som skal vises",
+ "rcfilters-limit-and-date-label": "$1 {{PLURAL:$1|ændring|ændringer}}, $2",
"rcfilters-days-title": "De sidste dage",
"rcfilters-hours-title": "De sidste timer",
"rcfilters-days-show-days": "$1 {{PLURAL:$1|dag|dage}}",
"rcfilters-days-show-hours": "$1 {{PLURAL:$1|time|timer}}",
"rcfilters-highlighted-filters-list": "Fremhævede: $1",
"rcfilters-quickfilters": "Gemte filtre",
- "rcfilters-quickfilters-placeholder-title": "Ingen links gemt endnu",
+ "rcfilters-quickfilters-placeholder-title": "Ingen filtre gemt endnu",
"rcfilters-savedqueries-defaultlabel": "Gemte filtre",
"rcfilters-savedqueries-rename": "Omdøb",
"rcfilters-savedqueries-setdefault": "Vælg som grundindstilling",
"rcfilters-savedqueries-unsetdefault": "Fravælg som grundindstilling",
- "rcfilters-savedqueries-remove": "Fjern",
+ "rcfilters-savedqueries-remove": "Slet",
"rcfilters-savedqueries-new-name-label": "Navn",
"rcfilters-savedqueries-new-name-placeholder": "Beskriv formålet med filteret",
"rcfilters-savedqueries-apply-label": "Opret filter",
"rcfilters-filtergroup-userExpLevel": "Brugerregistrering og -erfaring",
"rcfilters-filter-user-experience-level-registered-label": "Registrerede",
"rcfilters-filter-user-experience-level-registered-description": "Indloggede brugere",
- "rcfilters-filter-user-experience-level-unregistered-label": "Uregistrerede",
+ "rcfilters-filter-user-experience-level-unregistered-label": "Uregistreret",
"rcfilters-filter-user-experience-level-unregistered-description": "Redaktører, der ikke er logget ind.",
"rcfilters-filter-user-experience-level-newcomer-label": "Nybegyndere",
"rcfilters-filter-user-experience-level-newcomer-description": "Registrerede brugere som har færre end 10 redigeringer eller 4 dages aktivitet.",
"rcfilters-filter-user-experience-level-learner-label": "Let øvede",
- "rcfilters-filter-user-experience-level-learner-description": "Mere erfaring end \"nybegyndere\" men mindre end \"erfarne brugere\".",
+ "rcfilters-filter-user-experience-level-learner-description": "Registrerede brugere med mere erfaring end \"nybegyndere\" men mindre end \"erfarne brugere\".",
"rcfilters-filter-user-experience-level-experienced-label": "Erfarne brugere",
"rcfilters-filter-user-experience-level-experienced-description": "Registrerede skribenter med mere end 500 redigeringer og 30 dages aktivitet.",
"rcfilters-filtergroup-automated": "Automatiserede bidrag",
"rcfilters-filter-humans-label": "Menneske (ikke bot)",
"rcfilters-filter-humans-description": "Redigeringer udført af mennesker.",
"rcfilters-filtergroup-reviewstatus": "Gennemgangsstatus",
+ "rcfilters-filter-reviewstatus-unpatrolled-description": "Redigeringer som ikke er manuelt eller automatisk mærket som patruljerede.",
"rcfilters-filter-reviewstatus-unpatrolled-label": "Upatruljerede",
+ "rcfilters-filter-reviewstatus-manual-description": "Redigeringer som manuelt er mærket som patruljeret.",
+ "rcfilters-filter-reviewstatus-manual-label": "Patruljeret manuelt",
+ "rcfilters-filter-reviewstatus-auto-label": "Autopatruljeret",
"rcfilters-filtergroup-significance": "Betydning",
"rcfilters-filter-minor-label": "Mindre redigeringer",
"rcfilters-filter-minor-description": "Redigeringer som ophavsmanden har markeret som mindre.",
"backend-fail-read": "Kunne ikke læse filen $1.",
"backend-fail-create": "Kunne ikke gemme filen $1.",
"backend-fail-maxsize": "Kunne ikke gemme filen $1, da den er større end {{PLURAL:$2|en byte|$2 bytes}}.",
- "backend-fail-readonly": "Lagrings-backend \"$1\" er i øjeblikket skrivebeskyttet. Den angivne begrundelse var: \" $2 \"",
+ "backend-fail-readonly": "Lagrings-backend \"$1\" er i øjeblikket skrivebeskyttet. Den angivne begrundelse er: <em>$2</em>",
"backend-fail-synced": "Filen \"$1\" er i en inkonsistent tilstand inden for de interne lagringsbackends",
"backend-fail-connect": "Kunne ikke forbinde til lagringsbackend \"$1\".",
"backend-fail-internal": "En ukendt fejl opstod i filbackend \"$1\".",
"mycustomjsprotected": "No tiene permiso para editar esta página JavaScript.",
"myprivateinfoprotected": "No tiene permiso para editar su información privada.",
"mypreferencesprotected": "No tiene permiso para editar sus preferencias.",
- "exception-nologin-text": "Por favor inicie sesión para acceder a esta página o llevar a cabo esta acción.",
+ "exception-nologin-text": "Necesita acceder para ver esta página o llevar a cabo esta acción.",
"exception-nologin-text-manual": "Necesita $1 para poder ver esta página o llevar a cabo esta acción.",
"logouttext": "<strong>Su sesión ha finalizado.</strong>\n\nPuede que algunas páginas continúen mostrándose como si la sesión estuviera iniciada hasta que actualice la caché de su navegador.",
"welcomeuser": "Le damos la bienvenida, $1.",
"rcfilters-liveupdates-button-title-off": "Prikaži nove izmjene uživo",
"rcfilters-watchlist-markseen-button": "Označi sve izmjene kao pregledane",
"rcfilters-watchlist-edit-watchlist-button": "Izmijeni popis praćenih stranica",
+ "rcfilters-watchlist-showupdated": "Izmjene na stranicama koje niste posjetili otkako su se izmjene dogodile istaknute su <strong>podebljanim slovima</strong>, s ispunjenim kružićima.",
"rcfilters-preference-label": "Skrij poboljšanu inačicu nedavnih promjena",
"rcfilters-preference-help": "Vraća natrag stanje prije redizajna sučelja 2017., te svih oruđa dodanih tada i poslije toga.",
"rcfilters-watchlist-preference-label": "Sakrij poboljšanu inačicu popisa praćenja",
"nosuchusershort": "Ora ana panganggo mawa asma \"$1\". Coba dipriksa manèh pasang aksarané (éjaané).",
"nouserspecified": "Panjenengan kudu milih jeneng panganggo.",
"login-userblocked": "Panganggo iki pinalangan. Ora kena mbelu.",
- "wrongpassword": "Tembung wadi sing diisèkaké salah.\nMangga jajalen manèh.",
+ "wrongpassword": "Jenang panganggo utawa tembung wadi kang diisèkaké salah.\nMangga jajalen manèh.",
"wrongpasswordempty": "Tembung wadi kosong.\nJajalen manèh.",
"passwordtooshort": "Tembung sesinglon paling sethithik cacahé {{PLURAL:$1|1 aksara|$1 aksara}}.",
"passwordtoolong": "Tembung wadi ora kena munjuli {{PLURAL:$1|1 pralambang|$1 pralambang}}.",
"rcfilters-group-results-by-page": "Golongaké kasilé miturut kacané",
"rcfilters-activefilters": "Saringan murub",
"rcfilters-advancedfilters": "Saringan lanjutan",
- "rcfilters-limit-title": "Owahan-owahan sing arep dituduhaké",
+ "rcfilters-limit-title": "Kasil kang arep dituduhaké",
"rcfilters-days-title": "Dina-dina sing mentas waé",
"rcfilters-hours-title": "Jam-jam sing mentas waé",
"rcfilters-days-show-days": "$1 {{PLURAL:$1|dina|dina}}",
"rcfilters-quickfilters": "Saringan sumimpen",
- "rcfilters-quickfilters-placeholder-title": "Durung ana pranala sing disimpen",
+ "rcfilters-quickfilters-placeholder-title": "Durung ana saringan kang kasimpen",
"rcfilters-quickfilters-placeholder-description": "Saperlu nyimpen setèlaning saringan lan nganggo setèlan iku manèh ing tembé, kliken ikon markah buku ing babagan Saringan Murub ing ngisor.",
"rcfilters-savedqueries-defaultlabel": "Saringan sumimpen",
"rcfilters-savedqueries-rename": "Ganti jeneng",
"rcfilters-savedqueries-add-new-title": "Simpen setèlané saringan sing saiki",
"rcfilters-restore-default-filters": "Pulihaké saringan gawan",
"rcfilters-clear-all-filters": "Resiki kabèh saringan",
- "rcfilters-search-placeholder": "Saring owah-owahan anyar (lurua utawa wiwita ngetik)",
+ "rcfilters-search-placeholder": "Owah-owahan saringan (anggo menu utawa golèk jeneng saringan)",
"rcfilters-invalid-filter": "Saringan ora sah",
"rcfilters-empty-filter": "Ora ana saringan sing aktif. Kabèh sumbangan katuduhaké.",
"rcfilters-filterlist-title": "Saringan",
- "rcfilters-filterlist-whatsthis": "Apa iki?",
- "rcfilters-filterlist-feedbacklink": "Wènèhi saran ngenani saringan (béta) sing anyar",
+ "rcfilters-filterlist-whatsthis": "Kapiyé cara nganggo iki?",
+ "rcfilters-filterlist-feedbacklink": "Kandhani awak dhéwé panemumu bab piranti saringan iki",
"rcfilters-highlightbutton-title": "Sentrongi kasil",
"rcfilters-highlightmenu-title": "Pilih werna",
"rcfilters-highlightmenu-help": "Pilih werna kanggo nyentrong properti iki",
"tag-filter-submit": "Penyaring",
"tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Tenger|Tenger}}]]: $2)",
"tag-mw-contentmodelchange": "owahan modhèl isi",
+ "tag-mw-new-redirect": "Alihan anyar",
"tag-mw-blank": "Ngosongaké",
"tags-title": "Tag",
"tags-intro": "Kaca iki isi pratélan tenger sing dienggo nandhani besutan déning piranti alus, sinartan tegesé.",
"savechanges": "ცვლილებების შენახვა",
"publishpage": "გვერდის გამოქვეყნება",
"publishchanges": "ცვლილებების შენახვა",
+ "publishchanges-start": "ცვლილებების შენახვა…",
"preview": "წინასწარი გადახედვა",
"showpreview": "წინასწარი გადახედვის ჩვენება",
"showdiff": "ცვლილებების ჩვენება",
"postedit-confirmation-created": "გვერდი შეიქმნა.",
"postedit-confirmation-restored": "გვერდი აღდგა.",
"postedit-confirmation-saved": "თქვენი რედაქტირება შენახულია.",
+ "postedit-confirmation-published": "თქვენი რედაქტირება შენახულია.",
"edit-already-exists": "ახალი გვერდის შექმნა არ მოხერხდა.\nის უკვე არსებობს.",
"defaultmessagetext": "შეტყობინების სტანდარტული ტექსტი",
"content-failed-to-parse": "$2-ის შინაარსი არ შეესაბამება $1-ის ტიპს: $3.",
"rcfilters-watchlist-showupdated": "ცვლილებები გვერდებზე, რომლებიც თქვენ ჯერ არ გინახავთ ამ ცვლილებების გაკეთების შემდეგ, ნაჩვენებია <strong>მუქად</strong>, განსხვავებული ფერით.",
"rcfilters-preference-label": "ბოლო ცვლილებების გაუმჯობესებული ვერსიის დამალვა",
"rcfilters-preference-help": "გათიშავს 2017 წლის ინტერფეისის დიზაინზე გაკეთებულ განახლებას, გაითიშება მას შემდეგ დამატებული ყველა ხელსაწყო.",
+ "rcfilters-watchlist-preference-label": "კონტროლის სიის გაუმჯობესებული ვერსიის დამალვა",
+ "rcfilters-watchlist-preference-help": "აუქმებს 2017 წლის ინტერფეისისა და ყველა ხელსაწყოს რედიზაინს დამატებულს მაშინ და შემდგომ.",
"rcnotefrom": "ქვემოთ {{PLURAL:$5|ნაჩვენებია ცვლილება|ნაჩვენებია ცვლილებები}} <strong>$3, $4</strong>-დან (ნაჩვენებია არაუმეტეს <strong>$1</strong>).",
"rclistfromreset": "თარიღის საწყის კონფიგურაციაზე დაბრუნება",
"rclistfrom": "ახალი ცვლილებების ჩვენება დაწყებული $3 $2-დან",
"version-libraries-license": "Fahazoan-dalana",
"version-libraries-description": "Visavisa",
"version-libraries-authors": "Mpamorona",
+ "redirect-summary": "Ity pejy manokana ity dia manome fihodinana mankany amina rakitra (anaran-drakitra) na pejy (ID fiovana na pejy nomena). Fampiasana:\n[[{{#Special:Redirect}}/file/Example.jpg]],\n[[{{#Special:Redirect}}/page/64308]],\n[[{{#Special:Redirect}}/revision/328429]],\n[[{{#Special:Redirect}}/user/101]], na\n[[{{#Special:Redirect}}/logid/186]].",
"redirect-submit": "Alefa",
"redirect-lookup": "Karohana:",
"redirect-value": "Sanda:",
"tag-filter": "manasongadina [[Special:Tags|balizy]] :",
"tag-filter-submit": "Manasongadina",
"tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Marika}}]]: $2)",
+ "tag-mw-new-redirect": "Fihodinana vaovao",
"tags-title": "Balizy",
"tags-intro": "Ity pejy ity dia manalisitra ny balizy azon'ny rindrankajy ampiasaina mba hanamarika fanovana iray sy ny dikany.",
"tags-tag": "Anaran'ny balizy",
"revdelete-unrestricted": "fanerena nesorina tamin'ny mpandrindra",
"logentry-move-move": "nanova ny anaran'i $3 ho $4 i $1",
"logentry-move-move-noredirect": "$1{{GENDER:$2|}} dia namindra ny pejy $3 ho $4 fa tsy namela fihodinana",
+ "logentry-move-move_redir": "$1 dia namindra{{GENDER:$2|}} ny pejy $3 any amin'i $4 ary nanitsaka fihodinana",
+ "logentry-patrol-patrol-auto": "$1 dia nanamarika ho azy ny{{GENDER:$2|}} fiovana $4 ny pejy $3 ho voatety",
"logentry-newusers-newusers": "{{GENDER:$2|Noforonina}} ny kaontim-pikambana $1",
"logentry-newusers-create": "{{GENDER:$2|Noforonina}} ny kaontim-pikambana $1",
"logentry-newusers-create2": "{{GENDER:$2|Noforonin}}'i $1 ny kaomtim-pikambana $3",
"sort-descending": "Ordenar por ordem descendente",
"sort-ascending": "Ordenar por ordem ascendente",
"nstab-main": "Página",
- "nstab-user": "Página d{{GENDER:{{BASEPAGENAME}}|o usuário|a usuária|e usuário(a)}}",
+ "nstab-user": "Página {{GENDER:{{ROOTPAGENAME}}|do usuário|da usuária|de usuário(a)}}",
"nstab-media": "Página de mídia",
"nstab-special": "Página especial",
"nstab-project": "Página do projeto",
"revdelete-text-file": "Versões dos arquivos apagados continuarão a aparecer no arquivo de histórico, mas parte de seus conteúdos estarão inacessíveis ao público.",
"logdelete-text": "Eventos de log apagados continuarão a aparecer nos logs, mas parte de seus conteúdos estarão inacessíveis ao público.",
"revdelete-text-others": "Outros administradores do site {{SITENAME}} continuarão capazes de acessar o conteúdo oculto e podem apagá-lo pela mesma interface, a menos que restrições adicionais tenham sido feitas.",
- "revdelete-confirm": "Por favor confirme que pretende executar esta ação, que compreende as suas consequências e que o faz em concordância com as [[{{MediaWiki:Policy-url}}|políticas e recomendações]].",
+ "revdelete-confirm": "Por favor, confirme que pretende executar esta ação, que compreende as consequências dela e que o faz em concordância com as [[{{MediaWiki:Policy-url}}|políticas e recomendações]].",
"revdelete-suppress-text": "A supressão deverá ser usada '''apenas''' para os seguintes casos:\n* Informação potencialmente difamatória\n* Informação pessoal inapropriada\n*: ''endereços de domicílio e números de telefone, números da segurança social, etc''",
"revdelete-legend": "Definir restrições de visualização",
"revdelete-hide-text": "Texto de revisão",
"datedefault": "Sem preferência",
"prefs-labs": "Características de laboratório",
"prefs-user-pages": "Páginas de usuário",
- "prefs-personal": "Dados do usuário",
+ "prefs-personal": "Dados pessoais",
"prefs-rc": "Mudanças recentes",
"prefs-watchlist": "Lista de páginas vigiadas",
"prefs-editwatchlist": "Editar lista de páginas vigiadas",
"timezoneregion-pacific": "Oceano Pacífico",
"allowemail": "Permitir que outros usuários enviem-me e-mails",
"email-allow-new-users-label": "Permitir e-mails de novos usuários",
- "email-blacklist-label": "Proibir que estes usuários enviem-me e-mails:",
+ "email-blacklist-label": "Proibir que estes usuários me enviem e-mails:",
"prefs-searchoptions": "Busca",
"prefs-namespaces": "Espaços nominais",
"default": "padrão",
"markaspatrolledtext-file": "Marcar esta versão de artigo como patrulhada",
"markedaspatrolled": "Marcado como verificado",
"markedaspatrolledtext": "A revisão selecionada de [[:$1]] foi marcada como patrulhada.",
- "rcpatroldisabled": "Edições verificadas nas Mudanças Recentes desativadas",
- "rcpatroldisabledtext": "A funcionalidade de Edições verificadas nas Mudanças Recentes está atualmente desativada.",
+ "rcpatroldisabled": "Edições verificadas nas mudanças recentes desativadas",
+ "rcpatroldisabledtext": "A funcionalidade de edições verificadas nas mudanças recentes está atualmente desativada.",
"markedaspatrollederror": "Não é possível marcar como verificado",
"markedaspatrollederrortext": "Você precisa de especificar uma revisão para poder marcar como verificado.",
"markedaspatrollederror-noautopatrol": "Você não está autorizado a marcar suas próprias edições como edições patrulhadas.",
"watchlistedit-clear-explain": "Todos os títulos serão removidos da sua lista de páginas vigiadas",
"watchlistedit-clear-titles": "Títulos:",
"watchlistedit-clear-submit": "Limpar a lista de páginas vigiadas (Esta ação é permanente!)",
- "watchlistedit-clear-done": "Sua lista de páginas vigiadas foi limpa.",
+ "watchlistedit-clear-done": "Sua lista de páginas vigiadas foi esvaziada.",
"watchlistedit-clear-jobqueue": "A sua lista de páginas vigiadas está a ser esvaziada. Esta operação pode ser demorada.",
"watchlistedit-clear-removed": "{{PLURAL:$1|Foi removido um título|Foram removidos $1 títulos}}:",
"watchlistedit-too-many": "Há muitas páginas para exibir aqui.",
"sort-descending": "Ordenar por ordem descendente",
"sort-ascending": "Ordenar por ordem ascendente",
"nstab-main": "Página",
- "nstab-user": "Página {{GENDER:{{BASEPAGENAME}}|do utilizador|da utilizadora}}",
+ "nstab-user": "Página {{GENDER:{{ROOTPAGENAME}}|do utilizador|da utilizadora}}",
"nstab-media": "Multimédia",
"nstab-special": "Página especial",
"nstab-project": "Página do projeto",
"datedefault": "Sem preferência",
"prefs-labs": "Funcionalidades dos laboratórios",
"prefs-user-pages": "Páginas de utilizador",
- "prefs-personal": "Dados do utilizador",
+ "prefs-personal": "Dados pessoais",
"prefs-rc": "Mudanças recentes",
"prefs-watchlist": "Páginas vigiadas",
"prefs-editwatchlist": "Editar lista de páginas vigiadas",
"markaspatrolledtext-file": "Marcar esta versão do ficheiro como patrulhada",
"markedaspatrolled": "Marcada como patrulhada",
"markedaspatrolledtext": "A edição selecionada de [[:$1]] foi marcada como patrulhada.",
- "rcpatroldisabled": "Edições patrulhadas nas Mudanças Recentes desativadas",
- "rcpatroldisabledtext": "A funcionalidade de edições patrulhadas nas Mudanças Recentes está atualmente desativada.",
+ "rcpatroldisabled": "Edições patrulhadas nas mudanças recentes desativadas",
+ "rcpatroldisabledtext": "A funcionalidade de edições patrulhadas nas mudanças recentes está atualmente desativada.",
"markedaspatrollederror": "Não é possível marcar como patrulhada",
"markedaspatrollederrortext": "É necessário especificar uma edição a ser marcada como patrulhada.",
"markedaspatrollederror-noautopatrol": "Não está autorizado a marcar as suas próprias edições como edições patrulhadas.",
"watchlistedit-clear-explain": "Todos os títulos serão removidos da sua lista de páginas vigiadas.",
"watchlistedit-clear-titles": "Páginas:",
"watchlistedit-clear-submit": "Limpar páginas vigiadas (isto é permanente!)",
- "watchlistedit-clear-done": "A sua lista de páginas vigiadas foi limpa.",
+ "watchlistedit-clear-done": "A sua lista de páginas vigiadas foi esvaziada.",
"watchlistedit-clear-jobqueue": "A sua lista de páginas vigiadas está a ser esvaziada. Esta operação pode ser demorada.",
"watchlistedit-clear-removed": "{{PLURAL:$1|1 página foi removida|$1 páginas foram removidas}}:",
"watchlistedit-too-many": "Existem demasiadas páginas para apresentar.",
"exif-software": "مستعمل منطقگري",
"exif-artist": "ليکڪ",
"exif-copyright": "حق ۽ واسطا رکندڙ",
+ "exif-exifversion": "اِي ايڪس آئي ايف ورشن",
"exif-colorspace": "رنگ پولار",
"exif-pixelxdimension": "عڪس جي ويڪر",
"exif-pixelydimension": "عڪس جي اوچائي",
"rcfilters-watchlist-showupdated": "Zmeny stránok, ktoré ste od ich zmeny nenavštívili, sú zobrazené <strong>hrubo</strong> s vyplneným krúžkom.",
"rcfilters-preference-label": "Skryť vylepšenú verziu posledných úprav",
"rcfilters-preference-help": "Zruší novú podobu rozhrania z roku 2017 a všetky nástroje odvtedy pridané.",
+ "rcfilters-watchlist-preference-label": "Skryť vylepšenú verziu sledovaných stránok",
+ "rcfilters-watchlist-preference-help": "Zruší novú podobu rozhrania z roku 2017 a všetky nástroje odvtedy pridané.",
"rcnotefrom": "Nižšie {{PLURAL:$5|je zobrazená úprava|sú zobrazené úpravy}} od <strong>$2</strong> (do <strong>$1</strong>).",
"rclistfromreset": "Obnoviť výber údajov",
"rclistfrom": "Zobraziť nové úpravy počnúc od $3 $2",
"unblocked-id": "Blokovanie $1 bolo odstránené",
"unblocked-ip": "Adresa [[Special:Contributions/$1|$1]] bola odblokovaná.",
"blocklist": "Zablokovaní používatelia",
+ "autoblocklist": "Automatické blokovania",
+ "autoblocklist-submit": "Hľadať",
+ "autoblocklist-legend": "Zoznam automatických blokovaní",
+ "autoblocklist-localblocks": "Miestne automatické {{PLURAL:$1|blokovanie|blokovania}}",
+ "autoblocklist-total-autoblocks": "Celkový počet automatických blokovaní: $1",
+ "autoblocklist-empty": "Zoznam automatických blokovaní je prázdny.",
+ "autoblocklist-otherblocks": "Iné automatické {{PLURAL:$1|blokovanie|blokovania}}",
"ipblocklist": "Zablokovaní používatelia",
"ipblocklist-legend": "Nájsť zablokovaného používateľa",
"blocklist-userblocks": "Skryť blokovania účtov",
"variants": "Варијанте",
"navigation-heading": "Навигациони мени",
"errorpagetitle": "Грешка",
- "returnto": "Назад на $1.",
+ "returnto": "Назад на страницу „$1“.",
"tagline": "Извор: {{SITENAME}}",
"help": "Помоћ",
"search": "Претрага",
"tooltip-ca-watch": "Додајте ову страницу на свој списак надгледања",
"tooltip-ca-unwatch": "Уклоните ову страницу са списка надгледања",
"tooltip-search": "Претражите пројекат {{SITENAME}}",
- "tooltip-search-go": "Идите на страницу са тачно овим именом ако постоји",
+ "tooltip-search-go": "Идите на страницу с тачно овим именом ако постоји",
"tooltip-search-fulltext": "Претражите странице са овим текстом",
"tooltip-p-logo": "Посетите главну страну",
"tooltip-n-mainpage": "Посетите главну страну",
"rcfilters-liveupdates-button": "Canlı güncelleme",
"rcfilters-liveupdates-button-title-on": "Canlı güncellemeyi kapat",
"rcfilters-liveupdates-button-title-off": "Yeni değişiklikleri yapıldıkları anda görüntüleyin",
- "rcfilters-watchlist-markseen-button": "Tüm değişiklileri görüldü olarak işaretle",
+ "rcfilters-watchlist-markseen-button": "Tüm değişiklikleri görüldü olarak işaretle",
"rcfilters-watchlist-edit-watchlist-button": "İzlenen sayfaların listesini düzenle",
"rcfilters-target-page-placeholder": "Bir sayfa (ya da kategori) adı girin",
"rcnotefrom": "<strong>$3, $4</strong> tarihinden itibaren yapılan {{PLURAL:$5|değişiklik|değişiklik}} aşağıdadır (<strong>$1</strong> tarhine kadar olanlar gösterilmektedir).",
"tog-watchlisthideminor": "隱藏監視清單中的次要修訂",
"tog-watchlisthideliu": "隱藏監視清單中已登入使用者的編輯",
"tog-watchlistreloadautomatically": "篩選條件變更時自動重新讀取監視清單(需要使用 JavaScript)",
- "tog-watchlistunwatchlinks": "為帶有變更的監試頁面添加直接(取消)監視標記({{int:Watchlist-unwatch}}/{{int:Watchlist-unwatch-undo}}),需要 JavaScript 來打開功能",
+ "tog-watchlistunwatchlinks": "為有更改的監視頁面添加直接(取消)監視標記({{int:Watchlist-unwatch}}/{{int:Watchlist-unwatch-undo}},需要JavaScript才能開啟功能)",
"tog-watchlisthideanons": "隱藏監視清單中匿名使用者的編輯",
"tog-watchlisthidepatrolled": "隱藏監視清單中已巡查的編輯",
"tog-watchlisthidecategorization": "隱藏頁面分類",
$elapsed = microtime( true ) - $t0;
$this->writeln( "Done. Processed $this->totalCount rows in $elapsed seconds" );
+ return true;
}
/**
Object.keys( inspect.reports );
reports.forEach( function ( name ) {
+ if ( console.group ) {
+ console.group( 'mw.inspect ' + name + ' report' );
+ } else {
+ console.log( 'mw.inspect ' + name + ' report' );
+ }
inspect.dumpTable( inspect.reports[ name ]() );
+ if ( console.group ) {
+ console.groupEnd( 'mw.inspect ' + name + ' report' );
+ }
} );
};
$( '<div>' ).addClass( 'mw-navigation-hint' )
.text( mw.msg( 'prefs-tabs-navigation-hint' ) )
.attr( 'tabIndex', 0 )
- .on( 'focus blur', function ( e ) {
- if ( e.type === 'blur' || e.type === 'focusout' ) {
- $( this ).css( 'height', '0' );
- } else {
- $( this ).css( 'height', 'auto' );
- }
- } ).prependTo( '#mw-content-text' );
+ .prependTo( '#mw-content-text' );
tabs = new OO.ui.IndexLayout( {
expanded: false,
/*
* Hide, but keep accessible for screen-readers.
*/
-.client-js .mw-navigation-hint {
- overflow: hidden;
- height: 0;
- zoom: 1;
+.client-js .mw-navigation-hint:not( :focus ) {
+ .mixin-screen-reader-text;
}
/* Override OOUI styles so that dropdowns near the bottom of the form don't get clipped,
'HamcrestPHPUnitIntegration' => "$testDir/phpunit/HamcrestPHPUnitIntegration.php",
# tests/phpunit/includes
+ 'PageArchiveTestBase' => "$testDir/phpunit/includes/page/PageArchiveTestBase.php",
'RevisionDbTestBase' => "$testDir/phpunit/includes/RevisionDbTestBase.php",
'RevisionTestModifyableContent' => "$testDir/phpunit/includes/RevisionTestModifyableContent.php",
'RevisionTestModifyableContentHandler' => "$testDir/phpunit/includes/RevisionTestModifyableContentHandler.php",
// @codingStandardsIgnoreStart Generic.Files.LineLength
return [
[ false, '', '', '' ],
+ [
+ [ 'useNonces' => false ],
+ "script-src 'unsafe-eval' 'self' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&",
+ "script-src 'unsafe-eval' 'self' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&reportonly=1&",
+ "script-src 'unsafe-eval' 'self' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'"
+ ],
[
true,
"script-src 'unsafe-eval' 'self' 'nonce-secret' 'unsafe-inline' sister-site.somewhere.com *.wikipedia.org; default-src * data: blob:; style-src * data: blob: 'unsafe-inline'; report-uri /w/api.php?action=cspreport&format=json&",
/**
* @dataProvider providerCSPIsEnabled
- * @covers ContentSecurityPolicy::isEnabled
+ * @covers ContentSecurityPolicy::isNonceRequired
*/
public function testCSPIsEnabled( $main, $reportOnly, $expected ) {
global $wgCSPReportOnlyHeader, $wgCSPHeader;
global $wgCSPHeader;
$oldReport = wfSetVar( $wgCSPReportOnlyHeader, $reportOnly );
$oldMain = wfSetVar( $wgCSPHeader, $main );
- $res = ContentSecurityPolicy::isEnabled( RequestContext::getMain()->getConfig() );
+ $res = ContentSecurityPolicy::isNonceRequired( RequestContext::getMain()->getConfig() );
wfSetVar( $wgCSPReportOnlyHeader, $oldReport );
wfSetVar( $wgCSPHeader, $oldMain );
$this->assertEquals( $res, $expected );
[ false, [], true ],
[ [], false, true ],
[ [ 'default-src' => [ 'foo.example.com' ] ], false, true ],
+ [ [ 'useNonces' => false ], [ 'useNonces' => false ], false ],
+ [ [ 'useNonces' => true ], [ 'useNonces' => false ], true ],
+ [ [ 'useNonces' => false ], [ 'useNonces' => true ], true ],
];
}
}
+++ /dev/null
-<?php
-
-/**
- * Test class for page archiving.
- *
- * @group ContentHandler
- * @group Database
- * ^--- important, causes temporary tables to be used instead of the real database
- *
- * @group medium
- * ^--- important, causes tests not to fail with timeout
- */
-class PageArchiveTest extends MediaWikiTestCase {
-
- /**
- * @var PageArchive $archivedPage
- */
- private $archivedPage;
-
- /**
- * A logged out user who edited the page before it was archived.
- * @var string $ipEditor
- */
- private $ipEditor;
-
- /**
- * Revision ID of the IP edit
- * @var int $ipRevId
- */
- private $ipRevId;
-
- function __construct( $name = null, array $data = [], $dataName = '' ) {
- parent::__construct( $name, $data, $dataName );
-
- $this->tablesUsed = array_merge(
- $this->tablesUsed,
- [
- 'page',
- 'revision',
- 'ip_changes',
- 'text',
- 'archive',
- 'recentchanges',
- 'logging',
- 'page_props',
- ]
- );
- }
-
- protected function setUp() {
- parent::setUp();
-
- $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
- $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD );
- $this->setMwGlobals( 'wgMultiContentRevisionSchemaMigrationStage', SCHEMA_COMPAT_OLD );
- $this->overrideMwServices();
-
- // First create our dummy page
- $page = Title::newFromText( 'PageArchiveTest_thePage' );
- $page = new WikiPage( $page );
- $content = ContentHandler::makeContent(
- 'testing',
- $page->getTitle(),
- CONTENT_MODEL_WIKITEXT
- );
- $page->doEditContent( $content, 'testing', EDIT_NEW );
-
- // Insert IP revision
- $this->ipEditor = '2600:387:ed7:947e:8c16:a1ad:dd34:1dd7';
- $rev = new Revision( [
- 'text' => 'Lorem Ipsum',
- 'comment' => 'just a test',
- 'page' => $page->getId(),
- 'user_text' => $this->ipEditor,
- ] );
- $dbw = wfGetDB( DB_MASTER );
- $this->ipRevId = $rev->insertOn( $dbw );
-
- // Delete the page
- $page->doDeleteArticleReal( 'Just a test deletion' );
-
- $this->archivedPage = new PageArchive( $page->getTitle() );
- }
-
- /**
- * @covers PageArchive::undelete
- * @covers PageArchive::undeleteRevisions
- */
- public function testUndeleteRevisions() {
- // First make sure old revisions are archived
- $dbr = wfGetDB( DB_REPLICA );
- $arQuery = Revision::getArchiveQueryInfo();
- $res = $dbr->select(
- $arQuery['tables'],
- $arQuery['fields'],
- [ 'ar_rev_id' => $this->ipRevId ],
- __METHOD__,
- [],
- $arQuery['joins']
- );
- $row = $res->fetchObject();
- $this->assertEquals( $this->ipEditor, $row->ar_user_text );
-
- // Should not be in revision
- $res = $dbr->select( 'revision', '1', [ 'rev_id' => $this->ipRevId ] );
- $this->assertFalse( $res->fetchObject() );
-
- // Should not be in ip_changes
- $res = $dbr->select( 'ip_changes', '1', [ 'ipc_rev_id' => $this->ipRevId ] );
- $this->assertFalse( $res->fetchObject() );
-
- // Restore the page
- $this->archivedPage->undelete( [] );
-
- // Should be back in revision
- $revQuery = Revision::getQueryInfo();
- $res = $dbr->select(
- $revQuery['tables'],
- $revQuery['fields'],
- [ 'rev_id' => $this->ipRevId ],
- __METHOD__,
- [],
- $revQuery['joins']
- );
- $row = $res->fetchObject();
- $this->assertEquals( $this->ipEditor, $row->rev_user_text );
-
- // Should be back in ip_changes
- $res = $dbr->select( 'ip_changes', [ 'ipc_hex' ], [ 'ipc_rev_id' => $this->ipRevId ] );
- $row = $res->fetchObject();
- $this->assertEquals( IP::toHex( $this->ipEditor ), $row->ipc_hex );
- }
-
- /**
- * @covers PageArchive::listRevisions
- */
- public function testListRevisions() {
- $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
- $this->setMwGlobals( 'wgMultiContentRevisionSchemaMigrationStage', SCHEMA_COMPAT_OLD );
- $this->overrideMwServices();
-
- $revisions = $this->archivedPage->listRevisions();
- $this->assertEquals( 2, $revisions->numRows() );
-
- // Get the rows as arrays
- $row1 = (array)$revisions->current();
- $row2 = (array)$revisions->next();
- // Unset the timestamps (we assume they will be right...
- $this->assertInternalType( 'string', $row1['ar_timestamp'] );
- $this->assertInternalType( 'string', $row2['ar_timestamp'] );
- unset( $row1['ar_timestamp'] );
- unset( $row2['ar_timestamp'] );
-
- $this->assertEquals(
- [
- 'ar_minor_edit' => '0',
- 'ar_user' => '0',
- 'ar_user_text' => '2600:387:ed7:947e:8c16:a1ad:dd34:1dd7',
- 'ar_actor' => null,
- 'ar_len' => '11',
- 'ar_deleted' => '0',
- 'ar_rev_id' => '3',
- 'ar_sha1' => '0qdrpxl537ivfnx4gcpnzz0285yxryy',
- 'ar_page_id' => '2',
- 'ar_comment_text' => 'just a test',
- 'ar_comment_data' => null,
- 'ar_comment_cid' => null,
- 'ar_content_format' => null,
- 'ar_content_model' => null,
- 'ts_tags' => null,
- 'ar_id' => '2',
- 'ar_namespace' => '0',
- 'ar_title' => 'PageArchiveTest_thePage',
- 'ar_text_id' => '3',
- 'ar_parent_id' => '2',
- ],
- $row1
- );
- $this->assertEquals(
- [
- 'ar_minor_edit' => '0',
- 'ar_user' => '0',
- 'ar_user_text' => '127.0.0.1',
- 'ar_actor' => null,
- 'ar_len' => '7',
- 'ar_deleted' => '0',
- 'ar_rev_id' => '2',
- 'ar_sha1' => 'pr0s8e18148pxhgjfa0gjrvpy8fiyxc',
- 'ar_page_id' => '2',
- 'ar_comment_text' => 'testing',
- 'ar_comment_data' => null,
- 'ar_comment_cid' => null,
- 'ar_content_format' => null,
- 'ar_content_model' => null,
- 'ts_tags' => null,
- 'ar_id' => '1',
- 'ar_namespace' => '0',
- 'ar_title' => 'PageArchiveTest_thePage',
- 'ar_text_id' => '2',
- 'ar_parent_id' => '0',
- ],
- $row2
- );
- }
-
- /**
- * @covers PageArchive::listPagesBySearch
- */
- public function testListPagesBySearch() {
- $pages = PageArchive::listPagesBySearch( 'PageArchiveTest_thePage' );
- $this->assertSame( 1, $pages->numRows() );
-
- $page = (array)$pages->current();
-
- $this->assertSame(
- [
- 'ar_namespace' => '0',
- 'ar_title' => 'PageArchiveTest_thePage',
- 'count' => '2',
- ],
- $page
- );
- }
-
- /**
- * @covers PageArchive::listPagesBySearch
- */
- public function testListPagesByPrefix() {
- $pages = PageArchive::listPagesByPrefix( 'PageArchiveTest' );
- $this->assertSame( 1, $pages->numRows() );
-
- $page = (array)$pages->current();
-
- $this->assertSame(
- [
- 'ar_namespace' => '0',
- 'ar_title' => 'PageArchiveTest_thePage',
- 'count' => '2',
- ],
- $page
- );
- }
-
- /**
- * @covers PageArchive::getTextFromRow
- */
- public function testGetTextFromRow() {
- $row = (object)[ 'ar_text_id' => 2 ];
- $text = $this->archivedPage->getTextFromRow( $row );
- $this->assertSame( 'testing', $text );
- }
-
- /**
- * @covers PageArchive::getLastRevisionText
- */
- public function testGetLastRevisionText() {
- $text = $this->archivedPage->getLastRevisionText();
- $this->assertSame( 'Lorem Ipsum', $text );
- }
-
- /**
- * @covers PageArchive::isDeleted
- */
- public function testIsDeleted() {
- $this->assertTrue( $this->archivedPage->isDeleted() );
- }
-}
--- /dev/null
+<?php
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Tests\Storage\McrSchemaOverride;
+
+/**
+ * Test class for page archiving, using the new MCR schema.
+ *
+ * @group ContentHandler
+ * @group Database
+ * ^--- important, causes temporary tables to be used instead of the real database
+ *
+ * @group medium
+ * ^--- important, causes tests not to fail with timeout
+ */
+class PageArchiveMcrTest extends PageArchiveTestBase {
+
+ use McrSchemaOverride;
+
+ /**
+ * @covers PageArchive::listRevisions
+ */
+ public function testListRevisions_slots() {
+ $revisions = $this->archivedPage->listRevisions();
+
+ $revisionStore = MediaWikiServices::getInstance()->getInstance()->getRevisionStore();
+ $slotsQuery = $revisionStore->getSlotsQueryInfo( [ 'content' ] );
+
+ foreach ( $revisions as $row ) {
+ $this->assertSelect(
+ $slotsQuery['tables'],
+ 'count(*)',
+ [ 'slot_revision_id' => $row->ar_rev_id ],
+ [ [ 1 ] ],
+ [],
+ $slotsQuery['joins']
+ );
+ }
+ }
+
+ protected function getExpectedArchiveRows() {
+ return [
+ [
+ 'ar_minor_edit' => '0',
+ 'ar_user' => '0',
+ 'ar_user_text' => $this->ipEditor,
+ 'ar_actor' => null,
+ 'ar_len' => '11',
+ 'ar_deleted' => '0',
+ 'ar_rev_id' => strval( $this->ipRev->getId() ),
+ 'ar_timestamp' => $this->db->timestamp( $this->ipRev->getTimestamp() ),
+ 'ar_sha1' => '0qdrpxl537ivfnx4gcpnzz0285yxryy',
+ 'ar_page_id' => strval( $this->ipRev->getPageId() ),
+ 'ar_comment_text' => 'just a test',
+ 'ar_comment_data' => null,
+ 'ar_comment_cid' => null,
+ 'ts_tags' => null,
+ 'ar_id' => '2',
+ 'ar_namespace' => '0',
+ 'ar_title' => 'PageArchiveTest_thePage',
+ 'ar_parent_id' => strval( $this->ipRev->getParentId() ),
+ ],
+ [
+ 'ar_minor_edit' => '0',
+ 'ar_user' => (string)$this->getTestUser()->getUser()->getId(),
+ 'ar_user_text' => $this->getTestUser()->getUser()->getName(),
+ 'ar_actor' => null,
+ 'ar_len' => '7',
+ 'ar_deleted' => '0',
+ 'ar_rev_id' => strval( $this->firstRev->getId() ),
+ 'ar_timestamp' => $this->db->timestamp( $this->firstRev->getTimestamp() ),
+ 'ar_sha1' => 'pr0s8e18148pxhgjfa0gjrvpy8fiyxc',
+ 'ar_page_id' => strval( $this->firstRev->getPageId() ),
+ 'ar_comment_text' => 'testing',
+ 'ar_comment_data' => null,
+ 'ar_comment_cid' => null,
+ 'ts_tags' => null,
+ 'ar_id' => '1',
+ 'ar_namespace' => '0',
+ 'ar_title' => 'PageArchiveTest_thePage',
+ 'ar_parent_id' => '0',
+ ],
+ ];
+ }
+
+}
--- /dev/null
+<?php
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Storage\SqlBlobStore;
+use MediaWiki\Tests\Storage\PreMcrSchemaOverride;
+
+/**
+ * Test class for page archiving, using the pre-MCR schema.
+ *
+ * @group ContentHandler
+ * @group Database
+ * ^--- important, causes temporary tables to be used instead of the real database
+ *
+ * @group medium
+ * ^--- important, causes tests not to fail with timeout
+ */
+class PageArchivePreMcrTest extends PageArchiveTestBase {
+
+ use PreMcrSchemaOverride;
+
+ /**
+ * @covers PageArchive::getTextFromRow
+ */
+ public function testGetTextFromRow() {
+ $this->hideDeprecated( PageArchive::class . '::getTextFromRow' );
+
+ /** @var SqlBlobStore $blobStore */
+ $blobStore = MediaWikiServices::getInstance()->getBlobStore();
+
+ $textId = $blobStore->getTextIdFromAddress(
+ $this->firstRev->getSlot( 'main' )->getAddress()
+ );
+
+ $row = (object)[ 'ar_text_id' => $textId ];
+ $text = $this->archivedPage->getTextFromRow( $row );
+ $this->assertSame( 'testing', $text );
+ }
+
+ protected function getExpectedArchiveRows() {
+ /** @var SqlBlobStore $blobStore */
+ $blobStore = MediaWikiServices::getInstance()->getBlobStore();
+
+ return [
+ [
+ 'ar_minor_edit' => '0',
+ 'ar_user' => '0',
+ 'ar_user_text' => $this->ipEditor,
+ 'ar_actor' => null,
+ 'ar_len' => '11',
+ 'ar_deleted' => '0',
+ 'ar_rev_id' => strval( $this->ipRev->getId() ),
+ 'ar_timestamp' => $this->db->timestamp( $this->ipRev->getTimestamp() ),
+ 'ar_sha1' => '0qdrpxl537ivfnx4gcpnzz0285yxryy',
+ 'ar_page_id' => strval( $this->ipRev->getPageId() ),
+ 'ar_comment_text' => 'just a test',
+ 'ar_comment_data' => null,
+ 'ar_comment_cid' => null,
+ 'ar_content_format' => null,
+ 'ar_content_model' => null,
+ 'ts_tags' => null,
+ 'ar_id' => '2',
+ 'ar_namespace' => '0',
+ 'ar_title' => 'PageArchiveTest_thePage',
+ 'ar_text_id' => (string)$blobStore->getTextIdFromAddress(
+ $this->ipRev->getSlot( 'main' )->getAddress()
+ ),
+ 'ar_parent_id' => strval( $this->ipRev->getParentId() ),
+ ],
+ [
+ 'ar_minor_edit' => '0',
+ 'ar_user' => (string)$this->getTestUser()->getUser()->getId(),
+ 'ar_user_text' => $this->getTestUser()->getUser()->getName(),
+ 'ar_actor' => null,
+ 'ar_len' => '7',
+ 'ar_deleted' => '0',
+ 'ar_rev_id' => strval( $this->firstRev->getId() ),
+ 'ar_timestamp' => $this->db->timestamp( $this->firstRev->getTimestamp() ),
+ 'ar_sha1' => 'pr0s8e18148pxhgjfa0gjrvpy8fiyxc',
+ 'ar_page_id' => strval( $this->firstRev->getPageId() ),
+ 'ar_comment_text' => 'testing',
+ 'ar_comment_data' => null,
+ 'ar_comment_cid' => null,
+ 'ar_content_format' => null,
+ 'ar_content_model' => null,
+ 'ts_tags' => null,
+ 'ar_id' => '1',
+ 'ar_namespace' => '0',
+ 'ar_title' => 'PageArchiveTest_thePage',
+ 'ar_text_id' => (string)$blobStore->getTextIdFromAddress(
+ $this->firstRev->getSlot( 'main' )->getAddress()
+ ),
+ 'ar_parent_id' => '0',
+ ],
+ ];
+ }
+
+}
--- /dev/null
+<?php
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Storage\RevisionRecord;
+
+/**
+ * Base class for tests of PageArchive against different database schemas.
+ */
+abstract class PageArchiveTestBase extends MediaWikiTestCase {
+
+ /**
+ * @var int
+ */
+ protected $pageId;
+
+ /**
+ * @var PageArchive $archivedPage
+ */
+ protected $archivedPage;
+
+ /**
+ * A logged out user who edited the page before it was archived.
+ * @var string $ipEditor
+ */
+ protected $ipEditor;
+
+ /**
+ * Revision of the first (initial) edit
+ * @var RevisionRecord
+ */
+ protected $firstRev;
+
+ /**
+ * Revision of the IP edit (the second edit)
+ * @var RevisionRecord
+ */
+ protected $ipRev;
+
+ function __construct( $name = null, array $data = [], $dataName = '' ) {
+ parent::__construct( $name, $data, $dataName );
+
+ $this->tablesUsed = array_merge(
+ $this->tablesUsed,
+ [
+ 'page',
+ 'revision',
+ 'ip_changes',
+ 'text',
+ 'archive',
+ 'recentchanges',
+ 'logging',
+ 'page_props',
+ ]
+ );
+ }
+
+ protected function addCoreDBData() {
+ // Blank out to avoid failures when schema overrides imposed by subclasses
+ // affect revision storage.
+ }
+
+ /**
+ * @return int
+ */
+ abstract protected function getMcrMigrationStage();
+
+ /**
+ * @return string[]
+ */
+ abstract protected function getMcrTablesToReset();
+
+ /**
+ * @return bool
+ */
+ protected function getContentHandlerUseDB() {
+ return true;
+ }
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->tablesUsed += $this->getMcrTablesToReset();
+
+ $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD );
+ $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD );
+ $this->setMwGlobals( 'wgContentHandlerUseDB', $this->getContentHandlerUseDB() );
+ $this->setMwGlobals(
+ 'wgMultiContentRevisionSchemaMigrationStage',
+ $this->getMcrMigrationStage()
+ );
+ $this->overrideMwServices();
+
+ // First create our dummy page
+ $page = Title::newFromText( 'PageArchiveTest_thePage' );
+ $page = new WikiPage( $page );
+ $content = ContentHandler::makeContent(
+ 'testing',
+ $page->getTitle(),
+ CONTENT_MODEL_WIKITEXT
+ );
+
+ $user = $this->getTestUser()->getUser();
+ $page->doEditContent( $content, 'testing', EDIT_NEW, false, $user );
+
+ $this->pageId = $page->getId();
+ $this->firstRev = $page->getRevision()->getRevisionRecord();
+
+ // Insert IP revision
+ $this->ipEditor = '2001:db8::1';
+
+ $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
+
+ $ipTimestamp = wfTimestamp(
+ TS_MW,
+ wfTimestamp( TS_UNIX, $this->firstRev->getTimestamp() ) + 1
+ );
+
+ $rev = $revisionStore->newMutableRevisionFromArray( [
+ 'text' => 'Lorem Ipsum',
+ 'comment' => 'just a test',
+ 'page' => $page->getId(),
+ 'user_text' => $this->ipEditor,
+ 'timestamp' => $ipTimestamp,
+ ] );
+
+ $dbw = wfGetDB( DB_MASTER );
+ $this->ipRev = $revisionStore->insertRevisionOn( $rev, $dbw );
+
+ // Delete the page
+ $page->doDeleteArticleReal( 'Just a test deletion' );
+
+ $this->archivedPage = new PageArchive( $page->getTitle() );
+ }
+
+ /**
+ * @covers PageArchive::undelete
+ * @covers PageArchive::undeleteRevisions
+ */
+ public function testUndeleteRevisions() {
+ // TODO: MCR: Test undeletion with multiple slots. Check that slots remain untouched.
+
+ // First make sure old revisions are archived
+ $dbr = wfGetDB( DB_REPLICA );
+ $arQuery = Revision::getArchiveQueryInfo();
+ $row = $dbr->selectRow(
+ $arQuery['tables'],
+ $arQuery['fields'],
+ [ 'ar_rev_id' => $this->ipRev->getId() ],
+ __METHOD__,
+ [],
+ $arQuery['joins']
+ );
+ $this->assertEquals( $this->ipEditor, $row->ar_user_text );
+
+ // Should not be in revision
+ $row = $dbr->selectRow( 'revision', '1', [ 'rev_id' => $this->ipRev->getId() ] );
+ $this->assertFalse( $row );
+
+ // Should not be in ip_changes
+ $row = $dbr->selectRow( 'ip_changes', '1', [ 'ipc_rev_id' => $this->ipRev->getId() ] );
+ $this->assertFalse( $row );
+
+ // Restore the page
+ $this->archivedPage->undelete( [] );
+
+ // Should be back in revision
+ $revQuery = Revision::getQueryInfo();
+ $row = $dbr->selectRow(
+ $revQuery['tables'],
+ $revQuery['fields'],
+ [ 'rev_id' => $this->ipRev->getId() ],
+ __METHOD__,
+ [],
+ $revQuery['joins']
+ );
+ $this->assertNotFalse( $row, 'row exists in revision table' );
+ $this->assertEquals( $this->ipEditor, $row->rev_user_text );
+
+ // Should be back in ip_changes
+ $row = $dbr->selectRow( 'ip_changes', [ 'ipc_hex' ], [ 'ipc_rev_id' => $this->ipRev->getId() ] );
+ $this->assertNotFalse( $row, 'row exists in ip_changes table' );
+ $this->assertEquals( IP::toHex( $this->ipEditor ), $row->ipc_hex );
+ }
+
+ abstract protected function getExpectedArchiveRows();
+
+ /**
+ * @covers PageArchive::listRevisions
+ */
+ public function testListRevisions() {
+ $revisions = $this->archivedPage->listRevisions();
+ $this->assertEquals( 2, $revisions->numRows() );
+
+ // Get the rows as arrays
+ $row0 = (array)$revisions->current();
+ $row1 = (array)$revisions->next();
+
+ $expectedRows = $this->getExpectedArchiveRows();
+
+ $this->assertEquals(
+ $expectedRows[0],
+ $row0
+ );
+ $this->assertEquals(
+ $expectedRows[1],
+ $row1
+ );
+ }
+
+ /**
+ * @covers PageArchive::listPagesBySearch
+ */
+ public function testListPagesBySearch() {
+ $pages = PageArchive::listPagesBySearch( 'PageArchiveTest_thePage' );
+ $this->assertSame( 1, $pages->numRows() );
+
+ $page = (array)$pages->current();
+
+ $this->assertSame(
+ [
+ 'ar_namespace' => '0',
+ 'ar_title' => 'PageArchiveTest_thePage',
+ 'count' => '2',
+ ],
+ $page
+ );
+ }
+
+ /**
+ * @covers PageArchive::listPagesBySearch
+ */
+ public function testListPagesByPrefix() {
+ $pages = PageArchive::listPagesByPrefix( 'PageArchiveTest' );
+ $this->assertSame( 1, $pages->numRows() );
+
+ $page = (array)$pages->current();
+
+ $this->assertSame(
+ [
+ 'ar_namespace' => '0',
+ 'ar_title' => 'PageArchiveTest_thePage',
+ 'count' => '2',
+ ],
+ $page
+ );
+ }
+
+ public function provideGetTextFromRowThrowsInvalidArgumentException() {
+ yield 'missing ar_text_id field' => [ [] ];
+ yield 'ar_text_id is null' => [ [ 'ar_text_id' => null ] ];
+ yield 'ar_text_id is zero' => [ [ 'ar_text_id' => 0 ] ];
+ yield 'ar_text_id is "0"' => [ [ 'ar_text_id' => '0' ] ];
+ }
+
+ /**
+ * @dataProvider provideGetTextFromRowThrowsInvalidArgumentException
+ * @covers PageArchive::getTextFromRow
+ */
+ public function testGetTextFromRowThrowsInvalidArgumentException( array $row ) {
+ $this->hideDeprecated( PageArchive::class . '::getTextFromRow' );
+ $this->setExpectedException( InvalidArgumentException::class );
+
+ $this->archivedPage->getTextFromRow( (object)$row );
+ }
+
+ /**
+ * @covers PageArchive::getLastRevisionText
+ */
+ public function testGetLastRevisionText() {
+ $this->hideDeprecated( PageArchive::class . '::getLastRevisionText' );
+
+ $text = $this->archivedPage->getLastRevisionText();
+ $this->assertSame( 'Lorem Ipsum', $text );
+ }
+
+ /**
+ * @covers PageArchive::getLastRevisionId
+ */
+ public function testGetLastRevisionId() {
+ $id = $this->archivedPage->getLastRevisionId();
+ $this->assertSame( $this->ipRev->getId(), $id );
+ }
+
+ /**
+ * @covers PageArchive::isDeleted
+ */
+ public function testIsDeleted() {
+ $this->assertTrue( $this->archivedPage->isDeleted() );
+ }
+
+ /**
+ * @covers PageArchive::getRevision
+ */
+ public function testGetRevision() {
+ $rev = $this->archivedPage->getRevision( $this->ipRev->getTimestamp() );
+ $this->assertNotNull( $rev );
+ $this->assertSame( $this->pageId, $rev->getPage() );
+
+ $rev = $this->archivedPage->getRevision( '22991212115555' );
+ $this->assertNull( $rev );
+ }
+
+ /**
+ * @covers PageArchive::getRevision
+ */
+ public function testGetArchivedRevision() {
+ $rev = $this->archivedPage->getArchivedRevision( $this->ipRev->getId() );
+ $this->assertNotNull( $rev );
+ $this->assertSame( $this->ipRev->getTimestamp(), $rev->getTimestamp() );
+ $this->assertSame( $this->pageId, $rev->getPage() );
+
+ $rev = $this->archivedPage->getArchivedRevision( 632546 );
+ $this->assertNull( $rev );
+ }
+
+}
'2555',
'Thai year'
],
+ [
+ 'xkY',
+ '19410101090705',
+ '2484',
+ '2484',
+ 'Thai year'
+ ],
[
'xoY',
'20120102090705',