"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 ) {
* @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;
}
}
}
- /** Inserts a rollback link
+ /**
+ * Insert a rollback link
*
* @param string &$s
* @param RecentChange &$rc
if ( $rc->mAttribs['rc_type'] == RC_EDIT
&& $rc->mAttribs['rc_this_oldid']
&& $rc->mAttribs['rc_cur_id']
+ && $rc->getAttribute( 'page_latest' ) == $rc->mAttribs['rc_this_oldid']
) {
- $page = $rc->getTitle();
- /** Check for rollback and edit permissions, disallow special pages, and only
+ $title = $rc->getTitle();
+ /** Check for rollback permissions, disallow special pages, and only
* show a link on the top-most revision */
- if ( $this->getUser()->isAllowed( 'rollback' )
- && $rc->mAttribs['page_latest'] == $rc->mAttribs['rc_this_oldid']
- ) {
+ if ( $title->quickUserCan( 'rollback', $this->getUser() ) ) {
$rev = new Revision( [
- 'title' => $page,
+ 'title' => $title,
'id' => $rc->mAttribs['rc_this_oldid'],
'user' => $rc->mAttribs['rc_user'],
'user_text' => $rc->mAttribs['rc_user_text'],
*/
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,
}
function cutoffselector( $options ) {
- // Cast everything to strings immediately, so that we know all of the values have the same
- // precision, and can be compared with '==='. 2/24 has a few more decimal places than its
- // default string representation, for example, and would confuse comparisons.
-
- // Misleadingly, the 'days' option supports hours too.
- $days = array_map( 'strval', [ 1 / 24, 2 / 24, 6 / 24, 12 / 24, 1, 3, 7 ] );
-
- $userWatchlistOption = (string)$this->getUser()->getOption( 'watchlistdays' );
- // add the user preference, if it isn't available already
- if ( !in_array( $userWatchlistOption, $days ) && $userWatchlistOption !== '0' ) {
- $days[] = $userWatchlistOption;
- }
-
- $maxDays = (string)$this->maxDays;
- // add the maximum possible value, if it isn't available already
- if ( !in_array( $maxDays, $days ) ) {
- $days[] = $maxDays;
- }
-
- $selected = (string)$options['days'];
+ $selected = (float)$options['days'];
if ( $selected <= 0 ) {
- $selected = $maxDays;
- }
-
- // add the currently selected value, if it isn't available already
- if ( !in_array( $selected, $days ) ) {
- $days[] = $selected;
- }
+ $selected = $this->maxDays;
+ }
+
+ $selectedHours = round( $selected * 24 );
+
+ $hours = array_unique( array_filter( [
+ 1,
+ 2,
+ 6,
+ 12,
+ 24,
+ 72,
+ 168,
+ 24 * (float)$this->getUser()->getOption( 'watchlistdays', 0 ),
+ 24 * $this->maxDays,
+ $selectedHours
+ ] ) );
+ asort( $hours );
- $select = new XmlSelect( 'days', 'days', $selected );
+ $select = new XmlSelect( 'days', 'days', $selectedHours / 24 );
- asort( $days );
- foreach ( $days as $value ) {
- if ( $value < 1 ) {
- $name = $this->msg( 'hours' )->numParams( $value * 24 )->text();
+ foreach ( $hours as $value ) {
+ if ( $value < 24 ) {
+ $name = $this->msg( 'hours' )->numParams( $value )->text();
} else {
- $name = $this->msg( 'days' )->numParams( $value )->text();
+ $name = $this->msg( 'days' )->numParams( $value / 24 )->text();
}
- $select->addOption( $name, $value );
+ $select->addOption( $name, $value / 24 );
}
return $select->getHTML() . "\n<br />\n";
# 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;
}
/**
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',