maintenance/cleanupUsersWithNoId.php.
* $wgResourceLoaderMinifierStatementsOnOwnLine and $wgResourceLoaderMinifierMaxLineLength
were removed (deprecated since 1.27).
+* (T180921) $wgReferrerPolicy now supports having fallbacks for browsers that are not
+ using the latest version of the Referrer Policy specification.
=== New features in 1.31 ===
* Wikimedia\Rdbms\IDatabase->select() and similar methods now support
=== External library changes in 1.31 ===
==== Upgraded external libraries ====
+* Updated jquery.chosen from v0.9.14 to v1.8.2.
* …
==== New external libraries ====
* (T180052) Mirandese (mwl) now supports gendered NS_USER/NS_USER_TALK namespaces.
=== Other changes in 1.31 ===
+* Introducing multi-content-revision capability into the storage layer. For details,
+ see <https://www.mediawiki.org/wiki/Requests_for_comment/Multi-Content_Revisions>.
+* The Revision class was deprecated in favor of RevisionStore, BlobStore, and
+ RevisionRecord and its subclasses.
* MessageBlobStore::insertMessageBlob() (deprecated in 1.27) was removed.
* The global function wfBCP47 was renamed to LanguageCode::bcp47.
* The global function wfBCP47 is now deprecated.
* The Block class will no longer accept usable-but-missing usernames for
'byText' or ->setBlocker(). Callers should either ensure the blocker exists
locally or use a new interwiki-format username like "iw>Example".
+* The RevisionInsertComplete hook is now deprecated, use RevisionRecordInserted instead.
+ RevisionInsertComplete is still called, but the second and third parameter will always be null.
+ Hard deprecation is scheduled for 1.32.
* The following methods that get and set ParserOutput state are deprecated.
Callers should use the new stateless $options parameter to
ParserOutput::getText() instead.
* OutputPage::enableSectionEditLinks()
* OutputPage::sectionEditLinksEnabled()
* The public ParserOutput state fields $mTOCEnabled and $mEditSectionTokens are also deprecated.
+* The following methods and constants from the WatchedItem class were deprecated in
+ 1.27 have been removed.
+ * WatchedItem::getTitle()
+ * WatchedItem::fromUserTitle()
+ * WatchedItem::addWatch()
+ * WatchedItem::removeWatch()
+ * WatchedItem::isWatched()
+ * WatchedItem::duplicateEntries()
+ * WatchedItem::IGNORE_USER_RIGHTS
+ * WatchedItem::CHECK_USER_RIGHTS
+ * WatchedItem::DEPRECATED_USAGE_TIMESTAMP
+* The $statementsOnOwnLine parameter of JavaScriptMinifier::minify was removed.
+ The corresponding configuration variable ($wgResourceLoaderMinifierStatementsOnOwnLine)
+ has been deprecated since 1.27 and was removed as well.
== Compatibility ==
MediaWiki 1.31 requires PHP 5.5.9 or later. There is experimental support for
'Exif' => __DIR__ . '/includes/media/Exif.php',
'ExifBitmapHandler' => __DIR__ . '/includes/media/ExifBitmap.php',
'ExplodeIterator' => __DIR__ . '/includes/libs/ExplodeIterator.php',
- 'ExportProgressFilter' => __DIR__ . '/maintenance/backup.inc',
+ 'ExportProgressFilter' => __DIR__ . '/includes/export/ExportProgressFilter.php',
'ExportSites' => __DIR__ . '/maintenance/exportSites.php',
'ExtensionJsonValidationError' => __DIR__ . '/includes/registration/ExtensionJsonValidationError.php',
'ExtensionJsonValidator' => __DIR__ . '/includes/registration/ExtensionJsonValidator.php',
'MediaWiki\\Shell\\Result' => __DIR__ . '/includes/shell/Result.php',
'MediaWiki\\Shell\\Shell' => __DIR__ . '/includes/shell/Shell.php',
'MediaWiki\\Site\\MediaWikiPageNameNormalizer' => __DIR__ . '/includes/site/MediaWikiPageNameNormalizer.php',
+ 'MediaWiki\\Storage\\BlobAccessException' => __DIR__ . '/includes/Storage/BlobAccessException.php',
+ 'MediaWiki\\Storage\\BlobStore' => __DIR__ . '/includes/Storage/BlobStore.php',
+ 'MediaWiki\\Storage\\BlobStoreFactory' => __DIR__ . '/includes/Storage/BlobStoreFactory.php',
+ 'MediaWiki\\Storage\\IncompleteRevisionException' => __DIR__ . '/includes/Storage/IncompleteRevisionException.php',
+ 'MediaWiki\\Storage\\MutableRevisionRecord' => __DIR__ . '/includes/Storage/MutableRevisionRecord.php',
+ 'MediaWiki\\Storage\\MutableRevisionSlots' => __DIR__ . '/includes/Storage/MutableRevisionSlots.php',
+ 'MediaWiki\\Storage\\RevisionAccessException' => __DIR__ . '/includes/Storage/RevisionAccessException.php',
+ 'MediaWiki\\Storage\\RevisionArchiveRecord' => __DIR__ . '/includes/Storage/RevisionArchiveRecord.php',
+ 'MediaWiki\\Storage\\RevisionFactory' => __DIR__ . '/includes/Storage/RevisionFactory.php',
+ 'MediaWiki\\Storage\\RevisionLookup' => __DIR__ . '/includes/Storage/RevisionLookup.php',
+ 'MediaWiki\\Storage\\RevisionRecord' => __DIR__ . '/includes/Storage/RevisionRecord.php',
+ 'MediaWiki\\Storage\\RevisionSlots' => __DIR__ . '/includes/Storage/RevisionSlots.php',
+ 'MediaWiki\\Storage\\RevisionStore' => __DIR__ . '/includes/Storage/RevisionStore.php',
+ 'MediaWiki\\Storage\\RevisionStoreRecord' => __DIR__ . '/includes/Storage/RevisionStoreRecord.php',
+ 'MediaWiki\\Storage\\SlotRecord' => __DIR__ . '/includes/Storage/SlotRecord.php',
+ 'MediaWiki\\Storage\\SqlBlobStore' => __DIR__ . '/includes/Storage/SqlBlobStore.php',
+ 'MediaWiki\\Storage\\SuppressedDataException' => __DIR__ . '/includes/Storage/SuppressedDataException.php',
'MediaWiki\\Tidy\\BalanceActiveFormattingElements' => __DIR__ . '/includes/tidy/Balancer.php',
'MediaWiki\\Tidy\\BalanceElement' => __DIR__ . '/includes/tidy/Balancer.php',
'MediaWiki\\Tidy\\BalanceMarker' => __DIR__ . '/includes/tidy/Balancer.php',
'MediaWiki\\Tidy\\RemexMungerData' => __DIR__ . '/includes/tidy/RemexMungerData.php',
'MediaWiki\\Tidy\\TidyDriverBase' => __DIR__ . '/includes/tidy/TidyDriverBase.php',
'MediaWiki\\User\\UserIdentity' => __DIR__ . '/includes/user/UserIdentity.php',
+ 'MediaWiki\\User\\UserIdentityValue' => __DIR__ . '/includes/user/UserIdentityValue.php',
'MediaWiki\\Widget\\ComplexNamespaceInputWidget' => __DIR__ . '/includes/widget/ComplexNamespaceInputWidget.php',
'MediaWiki\\Widget\\ComplexTitleInputWidget' => __DIR__ . '/includes/widget/ComplexTitleInputWidget.php',
'MediaWiki\\Widget\\DateInputWidget' => __DIR__ . '/includes/widget/DateInputWidget.php',
in a Category page. Gives extensions the opportunity to batch load any
related data about the pages.
$type: The category type. Either 'page', 'file' or 'subcat'
-$res: Query result from DatabaseBase::select()
+$res: Query result from Wikimedia\Rdbms\IDatabase::select()
'CategoryViewer::generateLink': Before generating an output link allow
extensions opportunity to generate a more specific or relevant link.
Return false to stop further processing of the tag
$reader: XMLReader object
-'ImportHandleUnknownUser': When a user does exist locally, this hook is called
+'ImportHandleUnknownUser': When a user doesn't exist locally, this hook is called
to give extensions an opportunity to auto-create it. If the auto-creation is
successful, return false.
$name: User name
added to any module.
&$ResourceLoader: object
-'RevisionInsertComplete': Called after a revision is inserted into the database.
-&$revision: the Revision
-$data: the data stored in old_text. The meaning depends on $flags: if external
- is set, it's the URL of the revision text in external storage; otherwise,
- it's the revision text itself. In either case, if gzip is set, the revision
- text is gzipped.
-$flags: a comma-delimited list of strings representing the options used. May
- include: utf8 (this will always be set for new revisions); gzip; external.
+'RevisionRecordInserted': Called after a revision is inserted into the database.
+$revisionRecord: the RevisionRecord that has just been inserted.
+
+'RevisionInsertComplete': DEPRECATED! Use RevisionRecordInserted hook instead.
+Called after a revision is inserted into the database.
+$revision: the Revision
+$data: DEPRECATED! Always null!
+$flags: DEPRECATED! Always null!
'SearchableNamespaces': An option to modify which namespaces are searchable.
&$arr: Array of namespaces ($nsId => $name) which will be used.
}
}
-Autoloader::$psr4Namespaces = AutoLoader::getAutoloadNamespaces();
+AutoLoader::$psr4Namespaces = AutoLoader::getAutoloadNamespaces();
spl_autoload_register( [ 'AutoLoader', 'autoload' ] );
/**
* Value for the referrer policy meta tag.
- * One of 'never', 'default', 'origin', 'always'. Setting it to false just
- * prevents the meta tag from being output.
- * See https://www.w3.org/TR/referrer-policy/ for details.
- *
+ * One or more of the values defined in the Referrer Policy specification:
+ * https://w3c.github.io/webappsec-referrer-policy/
+ * ('no-referrer', 'no-referrer-when-downgrade', 'same-origin',
+ * 'origin', 'strict-origin', 'origin-when-cross-origin',
+ * 'strict-origin-when-cross-origin', or 'unsafe-url')
+ * Setting it to false prevents the meta tag from being output
+ * (which results in falling back to the Referrer-Policy header,
+ * or 'no-referrer-when-downgrade' if that's not set either.)
+ * Setting it to an array (supported since 1.31) will create a meta tag for
+ * each value, in the reverse of the order (meaning that the first array element
+ * will be the default and the others used as fallbacks for browsers which do not
+ * understand it).
+ *
+ * @var array|string|bool
* @since 1.25
*/
$wgReferrerPolicy = false;
* @code
* $wgResourceLoaderLESSVars = [
* 'exampleFontSize' => '1em',
- * 'exampleBlue' => '#eee',
+ * 'exampleBlue' => '#36c',
* ];
* @endcode
* @since 1.22
* - 'mw-blank': Edit completely blanks the page
* - 'mw-replace': Edit removes more than 90% of the content
* - 'mw-rollback': Edit is a rollback, made through the rollback link or rollback API
+ * - 'mw-undo': Edit made through an undo link
*
* @var array
* @since 1.31
'mw-changed-redirect-target' => true,
'mw-blank' => true,
'mw-replace' => true,
- 'mw-rollback' => true
+ 'mw-rollback' => true,
+ 'mw-undo' => true,
];
/**
}
/**
- * Get the unique id of this item
- *
+ * Get the unique id of this item; already xml-encoded
+ * @return string
+ */
+ public function getUniqueID() {
+ $id = $this->getUniqueIDUnescaped();
+ if ( $id ) {
+ return $this->xmlEncode( $id );
+ }
+ }
+
+ /**
+ * Get the unique id of this item, without any escaping
* @return string
*/
- public function getUniqueId() {
+ public function getUniqueIdUnescaped() {
if ( $this->uniqueId ) {
- return $this->xmlEncode( wfExpandUrl( $this->uniqueId, PROTO_CURRENT ) );
+ return wfExpandUrl( $this->uniqueId, PROTO_CURRENT );
}
}
return $this->xmlEncode( $this->url );
}
+ /** Get the URL of this item without any escaping
+ *
+ * @return string
+ */
+ public function getUrlUnescaped() {
+ return $this->url;
+ }
+
/**
* Get the description of this item; already xml-encoded
*
return $this->xmlEncode( $this->description );
}
+ /**
+ * Get the description of this item without any escaping
+ *
+ */
+ public function getDescriptionUnescaped() {
+ return $this->description;
+ }
+
/**
* Get the language of this item
*
return $this->xmlEncode( $this->author );
}
+ /**
+ * Get the author of this item without any escaping
+ *
+ * @return string
+ */
+ public function getAuthorUnescaped() {
+ return $this->author;
+ }
+
/**
* Get the comment of this item; already xml-encoded
*
return $this->xmlEncode( $this->comments );
}
+ /**
+ * Get the comment of this item without any escaping
+ *
+ * @return string
+ */
+ public function getCommentsUnescaped() {
+ return $this->comments;
+ }
+
/**
* Quickie hack... strip out wikilinks to more legible form from the comment.
*
* @ingroup Feed
*/
abstract class ChannelFeed extends FeedItem {
+
+ /** @var TemplateParser */
+ protected $templateParser;
+
+ /**
+ * @param string|Title $title Feed's title
+ * @param string $description
+ * @param string $url URL uniquely designating the feed.
+ * @param string $date Feed's date
+ * @param string $author Author's user name
+ * @param string $comments
+ */
+ function __construct( $title, $description, $url, $date = '', $author = '', $comments = '' ) {
+ parent::__construct( $title, $description, $url, $date, $author, $comments );
+ $this->templateParser = new TemplateParser();
+ }
+
/**
* Generate Header of the feed
* @par Example:
class RSSFeed extends ChannelFeed {
/**
- * Format a date given a timestamp
+ * Format a date given a timestamp. If a timestamp is not given, nothing is returned
*
- * @param int $ts Timestamp
- * @return string Date string
+ * @param int|null $ts Timestamp
+ * @return string|null Date string
*/
function formatTime( $ts ) {
- return gmdate( 'D, d M Y H:i:s \G\M\T', wfTimestamp( TS_UNIX, $ts ) );
+ if ( $ts ) {
+ return gmdate( 'D, d M Y H:i:s \G\M\T', wfTimestamp( TS_UNIX, $ts ) );
+ }
}
/**
global $wgVersion;
$this->outXmlHeader();
- ?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
- <channel>
- <title><?php print $this->getTitle() ?></title>
- <link><?php print wfExpandUrl( $this->getUrl(), PROTO_CURRENT ) ?></link>
- <description><?php print $this->getDescription() ?></description>
- <language><?php print $this->getLanguage() ?></language>
- <generator>MediaWiki <?php print $wgVersion ?></generator>
- <lastBuildDate><?php print $this->formatTime( wfTimestampNow() ) ?></lastBuildDate>
-<?php
+ // Manually escaping rather than letting Mustache do it because Mustache
+ // uses htmlentities, which does not work with XML
+ $templateParams = [
+ 'title' => $this->getTitle(),
+ 'url' => $this->xmlEncode( wfExpandUrl( $this->getUrlUnescaped(), PROTO_CURRENT ) ),
+ 'description' => $this->getDescription(),
+ 'language' => $this->xmlEncode( $this->getLanguage() ),
+ 'version' => $this->xmlEncode( $wgVersion ),
+ 'timestamp' => $this->xmlEncode( $this->formatTime( wfTimestampNow() ) )
+ ];
+ print $this->templateParser->processTemplate( 'RSSHeader', $templateParams );
}
/**
* @param FeedItem $item Item to be output
*/
function outItem( $item ) {
- // @codingStandardsIgnoreStart Ignore long lines and formatting issues.
- ?>
- <item>
- <title><?php print $item->getTitle(); ?></title>
- <link><?php print wfExpandUrl( $item->getUrl(), PROTO_CURRENT ); ?></link>
- <guid<?php if ( !$item->rssIsPermalink ) { print ' isPermaLink="false"'; } ?>><?php print $item->getUniqueId(); ?></guid>
- <description><?php print $item->getDescription() ?></description>
- <?php if ( $item->getDate() ) { ?><pubDate><?php print $this->formatTime( $item->getDate() ); ?></pubDate><?php } ?>
- <?php if ( $item->getAuthor() ) { ?><dc:creator><?php print $item->getAuthor(); ?></dc:creator><?php }?>
- <?php if ( $item->getComments() ) { ?><comments><?php print wfExpandUrl( $item->getComments(), PROTO_CURRENT ); ?></comments><?php }?>
- </item>
-<?php
- // @codingStandardsIgnoreEnd
+ // Manually escaping rather than letting Mustache do it because Mustache
+ // uses htmlentities, which does not work with XML
+ $templateParams = [
+ "title" => $item->getTitle(),
+ "url" => $this->xmlEncode( wfExpandUrl( $item->getUrlUnescaped(), PROTO_CURRENT ) ),
+ "permalink" => $item->rssIsPermalink,
+ "uniqueID" => $item->getUniqueId(),
+ "description" => $item->getDescription(),
+ "date" => $this->xmlEncode( $this->formatTime( $item->getDate() ) ),
+ "author" => $item->getAuthor()
+ ];
+ $comments = $item->getCommentsUnescaped();
+ if ( $comments ) {
+ $commentsEscaped = $this->xmlEncode( wfExpandUrl( $comments, PROTO_CURRENT ) );
+ $templateParams["comments"] = $commentsEscaped;
+ }
+ print $this->templateParser->processTemplate( 'RSSItem', $templateParams );
}
/**
* Output an RSS 2.0 footer
*/
function outFooter() {
- ?>
- </channel>
-</rss><?php
+ print "</channel></rss>";
}
}
*/
class AtomFeed extends ChannelFeed {
/**
- * Format a date given timestamp.
+ * Format a date given timestamp, if one is given.
*
- * @param string|int $timestamp
- * @return string
+ * @param string|int|null $timestamp
+ * @return string|null
*/
function formatTime( $timestamp ) {
- // need to use RFC 822 time format at least for rss2.0
- return gmdate( 'Y-m-d\TH:i:s', wfTimestamp( TS_UNIX, $timestamp ) );
+ if ( $timestamp ) {
+ // need to use RFC 822 time format at least for rss2.0
+ return gmdate( 'Y-m-d\TH:i:s', wfTimestamp( TS_UNIX, $timestamp ) );
+ }
}
/**
*/
function outHeader() {
global $wgVersion;
-
$this->outXmlHeader();
- // @codingStandardsIgnoreStart Ignore long lines and formatting issues.
- ?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="<?php print $this->getLanguage() ?>">
- <id><?php print $this->getFeedId() ?></id>
- <title><?php print $this->getTitle() ?></title>
- <link rel="self" type="application/atom+xml" href="<?php print wfExpandUrl( $this->getSelfUrl(), PROTO_CURRENT ) ?>"/>
- <link rel="alternate" type="text/html" href="<?php print wfExpandUrl( $this->getUrl(), PROTO_CURRENT ) ?>"/>
- <updated><?php print $this->formatTime( wfTimestampNow() ) ?>Z</updated>
- <subtitle><?php print $this->getDescription() ?></subtitle>
- <generator>MediaWiki <?php print $wgVersion ?></generator>
-
-<?php
- // @codingStandardsIgnoreEnd
+ // Manually escaping rather than letting Mustache do it because Mustache
+ // uses htmlentities, which does not work with XML
+ $templateParams = [
+ 'language' => $this->xmlEncode( $this->getLanguage() ),
+ 'feedID' => $this->getFeedID(),
+ 'title' => $this->getTitle(),
+ 'url' => $this->xmlEncode( wfExpandUrl( $this->getUrlUnescaped(), PROTO_CURRENT ) ),
+ 'selfUrl' => $this->getSelfUrl(),
+ 'timestamp' => $this->xmlEncode( $this->formatTime( wfTimestampNow() ) ),
+ 'description' => $this->getDescription(),
+ 'version' => $this->xmlEncode( $wgVersion ),
+ ];
+ print $this->templateParser->processTemplate( 'AtomHeader', $templateParams );
}
/**
*/
function outItem( $item ) {
global $wgMimeType;
- // @codingStandardsIgnoreStart Ignore long lines and formatting issues.
- ?>
- <entry>
- <id><?php print $item->getUniqueId(); ?></id>
- <title><?php print $item->getTitle(); ?></title>
- <link rel="alternate" type="<?php print $wgMimeType ?>" href="<?php print wfExpandUrl( $item->getUrl(), PROTO_CURRENT ); ?>"/>
- <?php if ( $item->getDate() ) { ?>
- <updated><?php print $this->formatTime( $item->getDate() ); ?>Z</updated>
- <?php } ?>
-
- <summary type="html"><?php print $item->getDescription() ?></summary>
- <?php if ( $item->getAuthor() ) { ?><author><name><?php print $item->getAuthor(); ?></name></author><?php }?>
- </entry>
-
-<?php /* @todo FIXME: Need to add comments
- <?php if( $item->getComments() ) { ?><dc:comment><?php print $item->getComments() ?></dc:comment><?php }?>
- */
+ // Manually escaping rather than letting Mustache do it because Mustache
+ // uses htmlentities, which does not work with XML
+ $templateParams = [
+ "uniqueID" => $item->getUniqueId(),
+ "title" => $item->getTitle(),
+ "mimeType" => $this->xmlEncode( $wgMimeType ),
+ "url" => $this->xmlEncode( wfExpandUrl( $item->getUrlUnescaped(), PROTO_CURRENT ) ),
+ "date" => $this->xmlEncode( $this->formatTime( $item->getDate() ) ),
+ "description" => $item->getDescription(),
+ "author" => $item->getAuthor()
+ ];
+ print $this->templateParser->processTemplate( 'AtomItem', $templateParams );
}
/**
* Outputs the footer for Atom 1.0 feed (basically '\</feed\>').
*/
- function outFooter() {?>
- </feed><?php
- // @codingStandardsIgnoreEnd
+ function outFooter() {
+ print "</feed>";
}
}
$op = $x['op'];
++$p;
switch ( $op ) {
- case self::XDL_BDOP_INS:
- $x = unpack( 'Csize', substr( $diff, $p, 1 ) );
- $p++;
- $out .= substr( $diff, $p, $x['size'] );
- $p += $x['size'];
- break;
- case self::XDL_BDOP_INSB:
- $x = unpack( 'Vcsize', substr( $diff, $p, 4 ) );
- $p += 4;
- $out .= substr( $diff, $p, $x['csize'] );
- $p += $x['csize'];
- break;
- case self::XDL_BDOP_CPY:
- $x = unpack( 'Voff/Vcsize', substr( $diff, $p, 8 ) );
- $p += 8;
- $out .= substr( $base, $x['off'], $x['csize'] );
- break;
- default:
- wfDebug( __METHOD__ . ": invalid op\n" );
- return false;
+ case self::XDL_BDOP_INS:
+ $x = unpack( 'Csize', substr( $diff, $p, 1 ) );
+ $p++;
+ $out .= substr( $diff, $p, $x['size'] );
+ $p += $x['size'];
+ break;
+ case self::XDL_BDOP_INSB:
+ $x = unpack( 'Vcsize', substr( $diff, $p, 4 ) );
+ $p += 4;
+ $out .= substr( $diff, $p, $x['csize'] );
+ $p += $x['csize'];
+ break;
+ case self::XDL_BDOP_CPY:
+ $x = unpack( 'Voff/Vcsize', substr( $diff, $p, 8 ) );
+ $p += 8;
+ $out .= substr( $base, $x['off'], $x['csize'] );
+ break;
+ default:
+ wfDebug( __METHOD__ . ": invalid op\n" );
+ return false;
}
}
return $out;
use Hooks;
use IBufferingStatsdDataFactory;
use MediaWiki\Shell\CommandFactory;
+use MediaWiki\Storage\BlobStore;
+use MediaWiki\Storage\BlobStoreFactory;
+use MediaWiki\Storage\RevisionStore;
use Wikimedia\Rdbms\LBFactory;
use LinkCache;
use Wikimedia\Rdbms\LoadBalancer;
return $this->getService( 'ExternalStoreFactory' );
}
+ /**
+ * @since 1.31
+ * @return BlobStoreFactory
+ */
+ public function getBlobStoreFactory() {
+ return $this->getService( 'BlobStoreFactory' );
+ }
+
+ /**
+ * @since 1.31
+ * @return BlobStore
+ */
+ public function getBlobStore() {
+ return $this->getService( '_SqlBlobStore' );
+ }
+
+ /**
+ * @since 1.31
+ * @return RevisionStore
+ */
+ public function getRevisionStore() {
+ return $this->getService( 'RevisionStore' );
+ }
+
///////////////////////////////////////////////////////////////////////////
// NOTE: When adding a service getter here, don't forget to add a test
// case for it in MediaWikiServicesTest::provideGetters() and in
*
* @file
*/
+use MediaWiki\MediaWikiServices;
use Wikimedia\Timestamp\TimestampException;
use Wikimedia\Rdbms\IDatabase;
}
$this->dest->invalidateCache(); // update histories
+ // Duplicate watchers of the old article to the new article on history merge
+ $store = MediaWikiServices::getInstance()->getWatchedItemStore();
+ $store->duplicateAllAssociatedEntries( $this->source, $this->dest );
+
// Update our logs
$logEntry = new ManualLogEntry( 'merge', 'merge' );
$logEntry->setPerformer( $user );
*/
protected function formatPlaintext( $plaintext, $format ) {
switch ( $format ) {
- case self::FORMAT_TEXT:
- case self::FORMAT_PLAIN:
- return $plaintext;
-
- case self::FORMAT_PARSE:
- case self::FORMAT_BLOCK_PARSE:
- case self::FORMAT_ESCAPED:
- default:
- return htmlspecialchars( $plaintext, ENT_QUOTES );
-
+ case self::FORMAT_TEXT:
+ case self::FORMAT_PLAIN:
+ return $plaintext;
+
+ case self::FORMAT_PARSE:
+ case self::FORMAT_BLOCK_PARSE:
+ case self::FORMAT_ESCAPED:
+ default:
+ return htmlspecialchars( $plaintext, ENT_QUOTES );
}
}
&& ( $relevantTitle->exists() || $relevantTitle->quickUserCan( 'create', $user ) );
foreach ( $title->getRestrictionTypes() as $type ) {
+ // Following keys are set in $vars:
+ // wgRestrictionCreate, wgRestrictionEdit, wgRestrictionMove, wgRestrictionUpload
$vars['wgRestriction' . ucfirst( $type )] = $title->getRestrictions( $type );
}
] );
if ( $config->get( 'ReferrerPolicy' ) !== false ) {
- $tags['meta-referrer'] = Html::element( 'meta', [
- 'name' => 'referrer',
- 'content' => $config->get( 'ReferrerPolicy' )
- ] );
+ // Per https://w3c.github.io/webappsec-referrer-policy/#unknown-policy-values
+ // fallbacks should come before the primary value so we need to reverse the array.
+ foreach ( array_reverse( (array)$config->get( 'ReferrerPolicy' ) ) as $i => $policy ) {
+ $tags["meta-referrer-$i"] = Html::element( 'meta', [
+ 'name' => 'referrer',
+ 'content' => $policy,
+ ] );
+ }
}
$p = "{$this->mIndexPolicy},{$this->mFollowPolicy}";
* @file
*/
-use Wikimedia\Rdbms\Database;
+use MediaWiki\Storage\MutableRevisionRecord;
+use MediaWiki\Storage\RevisionAccessException;
+use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Storage\RevisionStore;
+use MediaWiki\Storage\RevisionStoreRecord;
+use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Storage\SqlBlobStore;
+use MediaWiki\User\UserIdentityValue;
use Wikimedia\Rdbms\IDatabase;
use MediaWiki\Linker\LinkTarget;
use MediaWiki\MediaWikiServices;
use Wikimedia\Rdbms\FakeResultWrapper;
/**
- * @todo document
+ * @deprecated since 1.31, use RevisionRecord, RevisionStore, and BlobStore instead.
*/
class Revision implements IDBAccessObject {
- /** @var int|null */
- protected $mId;
- /** @var int|null */
- protected $mPage;
- /** @var string */
- protected $mUserText;
- /** @var string */
- protected $mOrigUserText;
- /** @var int */
- protected $mUser;
- /** @var bool */
- protected $mMinorEdit;
- /** @var string */
- protected $mTimestamp;
- /** @var int */
- protected $mDeleted;
- /** @var int */
- protected $mSize;
- /** @var string */
- protected $mSha1;
- /** @var int */
- protected $mParentId;
- /** @var string */
- protected $mComment;
- /** @var string */
- protected $mText;
- /** @var int */
- protected $mTextId;
- /** @var int */
- protected $mUnpatrolled;
-
- /** @var stdClass|null */
- protected $mTextRow;
-
- /** @var null|Title */
- protected $mTitle;
- /** @var bool */
- protected $mCurrent;
- /** @var string */
- protected $mContentModel;
- /** @var string */
- protected $mContentFormat;
-
- /** @var Content|null|bool */
- protected $mContent;
- /** @var null|ContentHandler */
- protected $mContentHandler;
-
- /** @var int */
- protected $mQueryFlags = 0;
- /** @var bool Used for cached values to reload user text and rev_deleted */
- protected $mRefreshMutableFields = false;
- /** @var string Wiki ID; false means the current wiki */
- protected $mWiki = false;
+
+ /** @var RevisionRecord */
+ protected $mRecord;
// Revision deletion constants
- const DELETED_TEXT = 1;
- const DELETED_COMMENT = 2;
- const DELETED_USER = 4;
- const DELETED_RESTRICTED = 8;
- const SUPPRESSED_USER = 12; // convenience
- const SUPPRESSED_ALL = 15; // convenience
+ const DELETED_TEXT = RevisionRecord::DELETED_TEXT;
+ const DELETED_COMMENT = RevisionRecord::DELETED_COMMENT;
+ const DELETED_USER = RevisionRecord::DELETED_USER;
+ const DELETED_RESTRICTED = RevisionRecord::DELETED_RESTRICTED;
+ const SUPPRESSED_USER = RevisionRecord::SUPPRESSED_USER;
+ const SUPPRESSED_ALL = RevisionRecord::SUPPRESSED_ALL;
// Audience options for accessors
- const FOR_PUBLIC = 1;
- const FOR_THIS_USER = 2;
- const RAW = 3;
+ const FOR_PUBLIC = RevisionRecord::FOR_PUBLIC;
+ const FOR_THIS_USER = RevisionRecord::FOR_THIS_USER;
+ const RAW = RevisionRecord::RAW;
+
+ const TEXT_CACHE_GROUP = SqlBlobStore::TEXT_CACHE_GROUP;
+
+ /**
+ * @return RevisionStore
+ */
+ protected static function getRevisionStore() {
+ return MediaWikiServices::getInstance()->getRevisionStore();
+ }
+
+ /**
+ * @param bool|string $wikiId The ID of the target wiki database. Use false for the local wiki.
+ *
+ * @return SqlBlobStore
+ */
+ protected static function getBlobStore( $wiki = false ) {
+ $store = MediaWikiServices::getInstance()
+ ->getBlobStoreFactory()
+ ->newSqlBlobStore( $wiki );
- const TEXT_CACHE_GROUP = 'revisiontext:10'; // process cache name and max key count
+ if ( !$store instanceof SqlBlobStore ) {
+ throw new RuntimeException(
+ 'The backwards compatibility code in Revision currently requires the BlobStore '
+ . 'service to be an SqlBlobStore instance, but it is a ' . get_class( $store )
+ );
+ }
+
+ return $store;
+ }
/**
* Load a page revision from a given revision ID number.
*
* @param int $id
* @param int $flags (optional)
+ * @param Title $title (optional) If known you can pass the Title in here.
+ * Passing no Title may result in another DB query if there are recent writes.
* @return Revision|null
*/
- public static function newFromId( $id, $flags = 0 ) {
- return self::newFromConds( [ 'rev_id' => intval( $id ) ], $flags );
+ public static function newFromId( $id, $flags = 0, Title $title = null ) {
+ /**
+ * MCR RevisionStore Compat
+ *
+ * If the title is not passed in as a param (already known) then select it here.
+ *
+ * Do the selection with MASTER if $flags includes READ_LATEST or recent changes
+ * have happened on our load balancer.
+ *
+ * If we select the title here and pass it down it will results in fewer queries
+ * further down the stack.
+ */
+ if ( !$title ) {
+ if (
+ $flags & self::READ_LATEST ||
+ wfGetLB()->hasOrMadeRecentMasterChanges()
+ ) {
+ $dbr = wfGetDB( DB_MASTER );
+ } else {
+ $dbr = wfGetDB( DB_REPLICA );
+ }
+ $row = $dbr->selectRow(
+ [ 'revision', 'page' ],
+ [
+ 'page_namespace',
+ 'page_title',
+ 'page_id',
+ 'page_latest',
+ 'page_is_redirect',
+ 'page_len',
+ ],
+ [ 'rev_id' => $id ],
+ __METHOD__,
+ [],
+ [ 'page' => [ 'JOIN', 'page_id=rev_page' ] ]
+ );
+ if ( $row ) {
+ $title = Title::newFromRow( $row );
+ }
+ wfGetLB()->reuseConnection( $dbr );
+ }
+
+ $rec = self::getRevisionStore()->getRevisionById( $id, $flags, $title );
+ return $rec === null ? null : new Revision( $rec, $flags, $title );
}
/**
* @return Revision|null
*/
public static function newFromTitle( LinkTarget $linkTarget, $id = 0, $flags = 0 ) {
- $conds = [
- 'page_namespace' => $linkTarget->getNamespace(),
- 'page_title' => $linkTarget->getDBkey()
- ];
- if ( $id ) {
- // Use the specified ID
- $conds['rev_id'] = $id;
- return self::newFromConds( $conds, $flags );
- } else {
- // Use a join to get the latest revision
- $conds[] = 'rev_id=page_latest';
- $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
- return self::loadFromConds( $db, $conds, $flags );
- }
+ $rec = self::getRevisionStore()->getRevisionByTitle( $linkTarget, $id, $flags );
+ return $rec === null ? null : new Revision( $rec, $flags );
}
/**
* @return Revision|null
*/
public static function newFromPageId( $pageId, $revId = 0, $flags = 0 ) {
- $conds = [ 'page_id' => $pageId ];
- if ( $revId ) {
- $conds['rev_id'] = $revId;
- return self::newFromConds( $conds, $flags );
- } else {
- // Use a join to get the latest revision
- $conds[] = 'rev_id = page_latest';
- $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
- return self::loadFromConds( $db, $conds, $flags );
- }
+ $rec = self::getRevisionStore()->getRevisionByPageId( $pageId, $revId, $flags );
+ return $rec === null ? null : new Revision( $rec, $flags );
}
/**
* Make a fake revision object from an archive table row. This is queried
* for permissions or even inserted (as in Special:Undelete)
- * @todo FIXME: Should be a subclass for RevisionDelete. [TS]
*
* @param object $row
* @param array $overrides
+ * @param Title $title (optional)
*
* @throws MWException
* @return Revision
*/
- public static function newFromArchiveRow( $row, $overrides = [] ) {
- global $wgContentHandlerUseDB;
-
- $attribs = $overrides + [
- 'page' => isset( $row->ar_page_id ) ? $row->ar_page_id : null,
- 'id' => isset( $row->ar_rev_id ) ? $row->ar_rev_id : null,
- 'comment' => CommentStore::newKey( 'ar_comment' )
- // Legacy because $row may have come from self::selectArchiveFields()
- ->getCommentLegacy( wfGetDB( DB_REPLICA ), $row, true )->text,
- 'user' => $row->ar_user,
- 'user_text' => $row->ar_user_text,
- 'timestamp' => $row->ar_timestamp,
- 'minor_edit' => $row->ar_minor_edit,
- 'text_id' => isset( $row->ar_text_id ) ? $row->ar_text_id : null,
- 'deleted' => $row->ar_deleted,
- 'len' => $row->ar_len,
- 'sha1' => isset( $row->ar_sha1 ) ? $row->ar_sha1 : null,
- 'content_model' => isset( $row->ar_content_model ) ? $row->ar_content_model : null,
- 'content_format' => isset( $row->ar_content_format ) ? $row->ar_content_format : null,
- ];
-
- if ( !$wgContentHandlerUseDB ) {
- unset( $attribs['content_model'] );
- unset( $attribs['content_format'] );
+ public static function newFromArchiveRow( $row, $overrides = [], Title $title = null ) {
+ /**
+ * MCR Migration: https://phabricator.wikimedia.org/T183564
+ * This method used to overwrite attributes, then passed to Revision::__construct
+ * RevisionStore::newRevisionFromArchiveRow instead overrides row field names
+ * So do a conversion here.
+ */
+ if ( array_key_exists( 'page', $overrides ) ) {
+ $overrides['page_id'] = $overrides['page'];
+ unset( $overrides['page'] );
}
- if ( !isset( $attribs['title'] )
- && isset( $row->ar_namespace )
- && isset( $row->ar_title )
- ) {
- $attribs['title'] = Title::makeTitle( $row->ar_namespace, $row->ar_title );
- }
-
- if ( isset( $row->ar_text ) && !$row->ar_text_id ) {
- // Pre-1.5 ar_text row
- $attribs['text'] = self::getRevisionText( $row, 'ar_' );
- if ( $attribs['text'] === false ) {
- throw new MWException( 'Unable to load text from archive row (possibly T24624)' );
- }
- }
- return new self( $attribs );
+ $rec = self::getRevisionStore()->newRevisionFromArchiveRow( $row, 0, $title, $overrides );
+ return new Revision( $rec, self::READ_NORMAL, $title );
}
/**
* @since 1.19
*
- * @param object $row
+ * MCR migration note: replaced by RevisionStore::newRevisionFromRow(). Note that
+ * newFromRow() also accepts arrays, while newRevisionFromRow() does not. Instead,
+ * a MutableRevisionRecord should be constructed directly. RevisionStore::newRevisionFromArray()
+ * can be used as a temporary replacement, but should be avoided.
+ *
+ * @param object|array $row
* @return Revision
*/
public static function newFromRow( $row ) {
- return new self( $row );
+ if ( is_array( $row ) ) {
+ $rec = self::getRevisionStore()->newMutableRevisionFromArray( $row );
+ } else {
+ $rec = self::getRevisionStore()->newRevisionFromRow( $row );
+ }
+
+ return new Revision( $rec );
}
/**
* Load a page revision from a given revision ID number.
* Returns null if no such revision can be found.
*
+ * @deprecated since 1.31, use RevisionStore::getRevisionById() instead.
+ *
* @param IDatabase $db
* @param int $id
* @return Revision|null
*/
public static function loadFromId( $db, $id ) {
- return self::loadFromConds( $db, [ 'rev_id' => intval( $id ) ] );
+ wfDeprecated( __METHOD__, '1.31' ); // no known callers
+ $rec = self::getRevisionStore()->loadRevisionFromId( $db, $id );
+ return $rec === null ? null : new Revision( $rec );
}
/**
* that's attached to a given page. If not attached
* to that page, will return null.
*
+ * @deprecated since 1.31, use RevisionStore::getRevisionByPageId() instead.
+ *
* @param IDatabase $db
* @param int $pageid
* @param int $id
* @return Revision|null
*/
public static function loadFromPageId( $db, $pageid, $id = 0 ) {
- $conds = [ 'rev_page' => intval( $pageid ), 'page_id' => intval( $pageid ) ];
- if ( $id ) {
- $conds['rev_id'] = intval( $id );
- } else {
- $conds[] = 'rev_id=page_latest';
- }
- return self::loadFromConds( $db, $conds );
+ $rec = self::getRevisionStore()->loadRevisionFromPageId( $db, $pageid, $id );
+ return $rec === null ? null : new Revision( $rec );
}
/**
* that's attached to a given page. If not attached
* to that page, will return null.
*
+ * @deprecated since 1.31, use RevisionStore::getRevisionByTitle() instead.
+ *
* @param IDatabase $db
* @param Title $title
* @param int $id
* @return Revision|null
*/
public static function loadFromTitle( $db, $title, $id = 0 ) {
- if ( $id ) {
- $matchId = intval( $id );
- } else {
- $matchId = 'page_latest';
- }
- return self::loadFromConds( $db,
- [
- "rev_id=$matchId",
- 'page_namespace' => $title->getNamespace(),
- 'page_title' => $title->getDBkey()
- ]
- );
+ $rec = self::getRevisionStore()->loadRevisionFromTitle( $db, $title, $id );
+ return $rec === null ? null : new Revision( $rec );
}
/**
* WARNING: Timestamps may in some circumstances not be unique,
* so this isn't the best key to use.
*
+ * @deprecated since 1.31, use RevisionStore::loadRevisionFromTimestamp() instead.
+ *
* @param IDatabase $db
* @param Title $title
* @param string $timestamp
* @return Revision|null
*/
public static function loadFromTimestamp( $db, $title, $timestamp ) {
- return self::loadFromConds( $db,
- [
- 'rev_timestamp' => $db->timestamp( $timestamp ),
- 'page_namespace' => $title->getNamespace(),
- 'page_title' => $title->getDBkey()
- ]
- );
- }
-
- /**
- * Given a set of conditions, fetch a revision
- *
- * This method is used then a revision ID is qualified and
- * will incorporate some basic replica DB/master fallback logic
- *
- * @param array $conditions
- * @param int $flags (optional)
- * @return Revision|null
- */
- private static function newFromConds( $conditions, $flags = 0 ) {
- $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_REPLICA );
-
- $rev = self::loadFromConds( $db, $conditions, $flags );
- // Make sure new pending/committed revision are visibile later on
- // within web requests to certain avoid bugs like T93866 and T94407.
- if ( !$rev
- && !( $flags & self::READ_LATEST )
- && wfGetLB()->getServerCount() > 1
- && wfGetLB()->hasOrMadeRecentMasterChanges()
- ) {
- $flags = self::READ_LATEST;
- $db = wfGetDB( DB_MASTER );
- $rev = self::loadFromConds( $db, $conditions, $flags );
- }
-
- if ( $rev ) {
- $rev->mQueryFlags = $flags;
- }
-
- return $rev;
- }
-
- /**
- * Given a set of conditions, fetch a revision from
- * the given database connection.
- *
- * @param IDatabase $db
- * @param array $conditions
- * @param int $flags (optional)
- * @return Revision|null
- */
- private static function loadFromConds( $db, $conditions, $flags = 0 ) {
- $row = self::fetchFromConds( $db, $conditions, $flags );
- if ( $row ) {
- $rev = new Revision( $row );
- $rev->mWiki = $db->getDomainID();
-
- return $rev;
- }
-
- return null;
+ // XXX: replace loadRevisionFromTimestamp by getRevisionByTimestamp?
+ $rec = self::getRevisionStore()->loadRevisionFromTimestamp( $db, $title, $timestamp );
+ return $rec === null ? null : new Revision( $rec );
}
/**
*
* @param LinkTarget $title
* @return ResultWrapper
- * @deprecated Since 1.28
+ * @deprecated Since 1.28, no callers in core nor in known extensions. No-op since 1.31.
*/
public static function fetchRevision( LinkTarget $title ) {
- $row = self::fetchFromConds(
- wfGetDB( DB_REPLICA ),
- [
- 'rev_id=page_latest',
- 'page_namespace' => $title->getNamespace(),
- 'page_title' => $title->getDBkey()
- ]
- );
-
- return new FakeResultWrapper( $row ? [ $row ] : [] );
- }
-
- /**
- * Given a set of conditions, return a ResultWrapper
- * which will return matching database rows with the
- * fields necessary to build Revision objects.
- *
- * @param IDatabase $db
- * @param array $conditions
- * @param int $flags (optional)
- * @return stdClass
- */
- private static function fetchFromConds( $db, $conditions, $flags = 0 ) {
- $revQuery = self::getQueryInfo( [ 'page', 'user' ] );
- $options = [];
- if ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) {
- $options[] = 'FOR UPDATE';
- }
- return $db->selectRow(
- $revQuery['tables'],
- $revQuery['fields'],
- $conditions,
- __METHOD__,
- $options,
- $revQuery['joins']
- );
+ wfDeprecated( __METHOD__, '1.31' );
+ return new FakeResultWrapper( [] );
}
/**
* Return the value of a select() JOIN conds array for the user table.
* This will get user table rows for logged-in users.
* @since 1.19
- * @deprecated since 1.31, use self::getQueryInfo( [ 'user' ] ) instead.
+ * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'user' ] ) instead.
* @return array
*/
public static function userJoinCond() {
* Return the value of a select() page conds array for the page table.
* This will assure that the revision(s) are not orphaned from live pages.
* @since 1.19
- * @deprecated since 1.31, use self::getQueryInfo( [ 'page' ] ) instead.
+ * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'page' ] ) instead.
* @return array
*/
public static function pageJoinCond() {
/**
* Return the list of revision fields that should be selected to create
* a new revision.
- * @deprecated since 1.31, use self::getQueryInfo() instead.
+ * @deprecated since 1.31, use RevisionStore::getQueryInfo() instead.
* @return array
*/
public static function selectFields() {
/**
* Return the list of revision fields that should be selected to create
* a new revision from an archive row.
- * @deprecated since 1.31, use self::getArchiveQueryInfo() instead.
+ * @deprecated since 1.31, use RevisionStore::getArchiveQueryInfo() instead.
* @return array
*/
public static function selectArchiveFields() {
/**
* Return the list of text fields that should be selected to read the
* revision text
- * @deprecated since 1.31, use self::getQueryInfo( [ 'text' ] ) instead.
+ * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'text' ] ) instead.
* @return array
*/
public static function selectTextFields() {
/**
* Return the list of page fields that should be selected from page table
- * @deprecated since 1.31, use self::getQueryInfo( [ 'page' ] ) instead.
+ * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'page' ] ) instead.
* @return array
*/
public static function selectPageFields() {
/**
* Return the list of user fields that should be selected from user table
- * @deprecated since 1.31, use self::getQueryInfo( [ 'user' ] ) instead.
+ * @deprecated since 1.31, use RevisionStore::getQueryInfo( [ 'user' ] ) instead.
* @return array
*/
public static function selectUserFields() {
* Return the tables, fields, and join conditions to be selected to create
* a new revision object.
* @since 1.31
+ * @deprecated since 1.31, use RevisionStore::getQueryInfo() instead.
* @param array $options Any combination of the following strings
* - 'page': Join with the page table, and select fields to identify the page
* - 'user': Join with the user table, and select the user name
* - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
*/
public static function getQueryInfo( $options = [] ) {
- global $wgContentHandlerUseDB;
-
- $commentQuery = CommentStore::newKey( 'rev_comment' )->getJoin();
- $ret = [
- 'tables' => [ 'revision' ] + $commentQuery['tables'],
- 'fields' => [
- 'rev_id',
- 'rev_page',
- 'rev_text_id',
- 'rev_timestamp',
- 'rev_user_text',
- 'rev_user',
- 'rev_minor_edit',
- 'rev_deleted',
- 'rev_len',
- 'rev_parent_id',
- 'rev_sha1',
- ] + $commentQuery['fields'],
- 'joins' => $commentQuery['joins'],
- ];
-
- if ( $wgContentHandlerUseDB ) {
- $ret['fields'][] = 'rev_content_format';
- $ret['fields'][] = 'rev_content_model';
- }
-
- if ( in_array( 'page', $options, true ) ) {
- $ret['tables'][] = 'page';
- $ret['fields'] = array_merge( $ret['fields'], [
- 'page_namespace',
- 'page_title',
- 'page_id',
- 'page_latest',
- 'page_is_redirect',
- 'page_len',
- ] );
- $ret['joins']['page'] = [ 'INNER JOIN', [ 'page_id = rev_page' ] ];
- }
-
- if ( in_array( 'user', $options, true ) ) {
- $ret['tables'][] = 'user';
- $ret['fields'] = array_merge( $ret['fields'], [
- 'user_name',
- ] );
- $ret['joins']['user'] = [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ];
- }
-
- if ( in_array( 'text', $options, true ) ) {
- $ret['tables'][] = 'text';
- $ret['fields'] = array_merge( $ret['fields'], [
- 'old_text',
- 'old_flags'
- ] );
- $ret['joins']['text'] = [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ];
- }
-
- return $ret;
+ return self::getRevisionStore()->getQueryInfo( $options );
}
/**
* Return the tables, fields, and join conditions to be selected to create
* a new archived revision object.
* @since 1.31
+ * @deprecated since 1.31, use RevisionStore::getArchiveQueryInfo() instead.
* @return array With three keys:
* - tables: (string[]) to include in the `$table` to `IDatabase->select()`
* - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
* - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
*/
public static function getArchiveQueryInfo() {
- global $wgContentHandlerUseDB;
-
- $commentQuery = CommentStore::newKey( 'ar_comment' )->getJoin();
- $ret = [
- 'tables' => [ 'archive' ] + $commentQuery['tables'],
- 'fields' => [
- 'ar_id',
- 'ar_page_id',
- 'ar_rev_id',
- 'ar_text',
- 'ar_text_id',
- 'ar_timestamp',
- 'ar_user_text',
- 'ar_user',
- 'ar_minor_edit',
- 'ar_deleted',
- 'ar_len',
- 'ar_parent_id',
- 'ar_sha1',
- ] + $commentQuery['fields'],
- 'joins' => $commentQuery['joins'],
- ];
-
- if ( $wgContentHandlerUseDB ) {
- $ret['fields'][] = 'ar_content_format';
- $ret['fields'][] = 'ar_content_model';
- }
-
- return $ret;
+ return self::getRevisionStore()->getArchiveQueryInfo();
}
/**
* @return array
*/
public static function getParentLengths( $db, array $revIds ) {
- $revLens = [];
- if ( !$revIds ) {
- return $revLens; // empty
- }
- $res = $db->select( 'revision',
- [ 'rev_id', 'rev_len' ],
- [ 'rev_id' => $revIds ],
- __METHOD__ );
- foreach ( $res as $row ) {
- $revLens[$row->rev_id] = $row->rev_len;
- }
- return $revLens;
+ return self::getRevisionStore()->listRevisionSizes( $db, $revIds );
}
/**
- * @param object|array $row Either a database row or an array
- * @throws MWException
+ * @param object|array|RevisionRecord $row Either a database row or an array
+ * @param int $queryFlags
+ * @param Title|null $title
+ *
* @access private
*/
- public function __construct( $row ) {
- if ( is_object( $row ) ) {
- $this->constructFromDbRowObject( $row );
- } elseif ( is_array( $row ) ) {
- $this->constructFromRowArray( $row );
- } else {
- throw new MWException( 'Revision constructor passed invalid row format.' );
- }
- $this->mUnpatrolled = null;
- }
-
- /**
- * @param object $row
- */
- private function constructFromDbRowObject( $row ) {
- $this->mId = intval( $row->rev_id );
- $this->mPage = intval( $row->rev_page );
- $this->mTextId = intval( $row->rev_text_id );
- $this->mComment = CommentStore::newKey( 'rev_comment' )
- // Legacy because $row may have come from self::selectFields()
- ->getCommentLegacy( wfGetDB( DB_REPLICA ), $row, true )->text;
- $this->mUser = intval( $row->rev_user );
- $this->mMinorEdit = intval( $row->rev_minor_edit );
- $this->mTimestamp = $row->rev_timestamp;
- $this->mDeleted = intval( $row->rev_deleted );
-
- if ( !isset( $row->rev_parent_id ) ) {
- $this->mParentId = null;
- } else {
- $this->mParentId = intval( $row->rev_parent_id );
- }
-
- if ( !isset( $row->rev_len ) ) {
- $this->mSize = null;
- } else {
- $this->mSize = intval( $row->rev_len );
- }
-
- if ( !isset( $row->rev_sha1 ) ) {
- $this->mSha1 = null;
- } else {
- $this->mSha1 = $row->rev_sha1;
- }
+ function __construct( $row, $queryFlags = 0, Title $title = null ) {
+ global $wgUser;
- if ( isset( $row->page_latest ) ) {
- $this->mCurrent = ( $row->rev_id == $row->page_latest );
- $this->mTitle = Title::newFromRow( $row );
- } else {
- $this->mCurrent = false;
- $this->mTitle = null;
- }
-
- if ( !isset( $row->rev_content_model ) ) {
- $this->mContentModel = null; # determine on demand if needed
- } else {
- $this->mContentModel = strval( $row->rev_content_model );
- }
-
- if ( !isset( $row->rev_content_format ) ) {
- $this->mContentFormat = null; # determine on demand if needed
- } else {
- $this->mContentFormat = strval( $row->rev_content_format );
- }
+ if ( $row instanceof RevisionRecord ) {
+ $this->mRecord = $row;
+ } elseif ( is_array( $row ) ) {
+ if ( !isset( $row['user'] ) && !isset( $row['user_text'] ) ) {
+ $row['user'] = $wgUser;
+ }
- // Lazy extraction...
- $this->mText = null;
- if ( isset( $row->old_text ) ) {
- $this->mTextRow = $row;
+ $this->mRecord = self::getRevisionStore()->newMutableRevisionFromArray(
+ $row,
+ $queryFlags,
+ $title
+ );
+ } elseif ( is_object( $row ) ) {
+ $this->mRecord = self::getRevisionStore()->newRevisionFromRow(
+ $row,
+ $queryFlags,
+ $title
+ );
} else {
- // 'text' table row entry will be lazy-loaded
- $this->mTextRow = null;
- }
-
- // Use user_name for users and rev_user_text for IPs...
- $this->mUserText = null; // lazy load if left null
- if ( $this->mUser == 0 ) {
- $this->mUserText = $row->rev_user_text; // IP user
- } elseif ( isset( $row->user_name ) ) {
- $this->mUserText = $row->user_name; // logged-in user
+ throw new InvalidArgumentException(
+ '$row must be a row object, an associative array, or a RevisionRecord'
+ );
}
- $this->mOrigUserText = $row->rev_user_text;
}
/**
- * @param array $row
- *
- * @throws MWException
+ * @return RevisionRecord
*/
- private function constructFromRowArray( array $row ) {
- // Build a new revision to be saved...
- global $wgUser; // ugh
-
- # if we have a content object, use it to set the model and type
- if ( !empty( $row['content'] ) ) {
- if ( !( $row['content'] instanceof Content ) ) {
- throw new MWException( '`content` field must contain a Content object.' );
- }
-
- // @todo when is that set? test with external store setup! check out insertOn() [dk]
- if ( !empty( $row['text_id'] ) ) {
- throw new MWException( "Text already stored in external store (id {$row['text_id']}), " .
- "can't serialize content object" );
- }
-
- $row['content_model'] = $row['content']->getModel();
- # note: mContentFormat is initializes later accordingly
- # note: content is serialized later in this method!
- # also set text to null?
- }
-
- $this->mId = isset( $row['id'] ) ? intval( $row['id'] ) : null;
- $this->mPage = isset( $row['page'] ) ? intval( $row['page'] ) : null;
- $this->mTextId = isset( $row['text_id'] ) ? intval( $row['text_id'] ) : null;
- $this->mUserText = isset( $row['user_text'] )
- ? strval( $row['user_text'] ) : $wgUser->getName();
- $this->mUser = isset( $row['user'] ) ? intval( $row['user'] ) : $wgUser->getId();
- $this->mMinorEdit = isset( $row['minor_edit'] ) ? intval( $row['minor_edit'] ) : 0;
- $this->mTimestamp = isset( $row['timestamp'] )
- ? strval( $row['timestamp'] ) : wfTimestampNow();
- $this->mDeleted = isset( $row['deleted'] ) ? intval( $row['deleted'] ) : 0;
- $this->mSize = isset( $row['len'] ) ? intval( $row['len'] ) : null;
- $this->mParentId = isset( $row['parent_id'] ) ? intval( $row['parent_id'] ) : null;
- $this->mSha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null;
-
- $this->mContentModel = isset( $row['content_model'] )
- ? strval( $row['content_model'] ) : null;
- $this->mContentFormat = isset( $row['content_format'] )
- ? strval( $row['content_format'] ) : null;
-
- // Enforce spacing trimming on supplied text
- $this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null;
- $this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
- $this->mTextRow = null;
-
- $this->mTitle = isset( $row['title'] ) ? $row['title'] : null;
-
- // if we have a Content object, override mText and mContentModel
- if ( !empty( $row['content'] ) ) {
- $handler = $this->getContentHandler();
- $this->mContent = $row['content'];
-
- $this->mContentModel = $this->mContent->getModel();
- $this->mContentHandler = null;
-
- $this->mText = $handler->serializeContent( $row['content'], $this->getContentFormat() );
- } elseif ( $this->mText !== null ) {
- $handler = $this->getContentHandler();
- $this->mContent = $handler->unserializeContent( $this->mText );
- }
-
- // If we have a Title object, make sure it is consistent with mPage.
- if ( $this->mTitle && $this->mTitle->exists() ) {
- if ( $this->mPage === null ) {
- // if the page ID wasn't known, set it now
- $this->mPage = $this->mTitle->getArticleID();
- } elseif ( $this->mTitle->getArticleID() !== $this->mPage ) {
- // Got different page IDs. This may be legit (e.g. during undeletion),
- // but it seems worth mentioning it in the log.
- wfDebug( "Page ID " . $this->mPage . " mismatches the ID " .
- $this->mTitle->getArticleID() . " provided by the Title object." );
- }
- }
-
- $this->mCurrent = false;
-
- // If we still have no length, see it we have the text to figure it out
- if ( !$this->mSize && $this->mContent !== null ) {
- $this->mSize = $this->mContent->getSize();
- }
-
- // Same for sha1
- if ( $this->mSha1 === null ) {
- $this->mSha1 = $this->mText === null ? null : self::base36Sha1( $this->mText );
- }
-
- // force lazy init
- $this->getContentModel();
- $this->getContentFormat();
+ public function getRevisionRecord() {
+ return $this->mRecord;
}
/**
* @return int|null
*/
public function getId() {
- return $this->mId;
+ return $this->mRecord->getId();
}
/**
* Set the revision ID
*
- * This should only be used for proposed revisions that turn out to be null edits
+ * This should only be used for proposed revisions that turn out to be null edits.
+ *
+ * @note Only supported on Revisions that were constructed based on associative arrays,
+ * since they are mutable.
*
* @since 1.19
- * @param int $id
+ * @param int|string $id
+ * @throws MWException
*/
public function setId( $id ) {
- $this->mId = (int)$id;
+ if ( $this->mRecord instanceof MutableRevisionRecord ) {
+ $this->mRecord->setId( intval( $id ) );
+ } else {
+ throw new MWException( __METHOD__ . ' is not supported on this instance' );
+ }
}
/**
*
* This should only be used for proposed revisions that turn out to be null edits
*
+ * @note Only supported on Revisions that were constructed based on associative arrays,
+ * since they are mutable.
+ *
* @since 1.28
* @deprecated since 1.31, please reuse old Revision object
* @param int $id User ID
* @param string $name User name
+ * @throws MWException
*/
public function setUserIdAndName( $id, $name ) {
- $this->mUser = (int)$id;
- $this->mUserText = $name;
- $this->mOrigUserText = $name;
+ if ( $this->mRecord instanceof MutableRevisionRecord ) {
+ $user = new UserIdentityValue( intval( $id ), $name );
+ $this->mRecord->setUser( $user );
+ } else {
+ throw new MWException( __METHOD__ . ' is not supported on this instance' );
+ }
}
/**
- * Get text row ID
+ * @return SlotRecord
+ */
+ private function getMainSlotRaw() {
+ return $this->mRecord->getSlot( 'main', RevisionRecord::RAW );
+ }
+
+ /**
+ * Get the ID of the row of the text table that contains the content of the
+ * revision's main slot, if that content is stored in the text table.
+ *
+ * If the content is stored elsewhere, this returns null.
+ *
+ * @deprecated since 1.31, use RevisionRecord()->getSlot()->getContentAddress() to
+ * get that actual address that can be used with BlobStore::getBlob(); or use
+ * RevisionRecord::hasSameContent() to check if two revisions have the same content.
*
* @return int|null
*/
public function getTextId() {
- return $this->mTextId;
+ $slot = $this->getMainSlotRaw();
+ return $slot->hasAddress()
+ ? self::getBlobStore()->getTextIdFromAddress( $slot->getAddress() )
+ : null;
}
/**
* Get parent revision ID (the original previous page revision)
*
- * @return int|null
+ * @return int|null The ID of the parent revision. 0 indicates that there is no
+ * parent revision. Null indicates that the parent revision is not known.
*/
public function getParentId() {
- return $this->mParentId;
+ return $this->mRecord->getParentId();
}
/**
* Returns the length of the text in this revision, or null if unknown.
*
- * @return int|null
+ * @return int
*/
public function getSize() {
- return $this->mSize;
+ return $this->mRecord->getSize();
}
/**
- * Returns the base36 sha1 of the text in this revision, or null if unknown.
+ * Returns the base36 sha1 of the content in this revision, or null if unknown.
*
- * @return string|null
+ * @return string
*/
public function getSha1() {
- return $this->mSha1;
+ // XXX: we may want to drop all the hashing logic, it's not worth the overhead.
+ return $this->mRecord->getSha1();
}
/**
- * Returns the title of the page associated with this entry or null.
+ * Returns the title of the page associated with this entry.
+ * Since 1.31, this will never return null.
*
* Will do a query, when title is not set and id is given.
*
- * @return Title|null
+ * @return Title
*/
public function getTitle() {
- if ( $this->mTitle !== null ) {
- return $this->mTitle;
- }
- // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
- if ( $this->mId !== null ) {
- $dbr = wfGetLB( $this->mWiki )->getConnectionRef( DB_REPLICA, [], $this->mWiki );
- // @todo: Title::getSelectFields(), or Title::getQueryInfo(), or something like that
- $row = $dbr->selectRow(
- [ 'revision', 'page' ],
- [
- 'page_namespace',
- 'page_title',
- 'page_id',
- 'page_latest',
- 'page_is_redirect',
- 'page_len',
- ],
- [ 'rev_id' => $this->mId ],
- __METHOD__,
- [],
- [ 'page' => [ 'JOIN', 'page_id=rev_page' ] ]
- );
- if ( $row ) {
- // @TODO: better foreign title handling
- $this->mTitle = Title::newFromRow( $row );
- }
- }
-
- if ( $this->mWiki === false || $this->mWiki === wfWikiID() ) {
- // Loading by ID is best, though not possible for foreign titles
- if ( !$this->mTitle && $this->mPage !== null && $this->mPage > 0 ) {
- $this->mTitle = Title::newFromID( $this->mPage );
- }
- }
-
- return $this->mTitle;
+ $linkTarget = $this->mRecord->getPageAsLinkTarget();
+ return Title::newFromLinkTarget( $linkTarget );
}
/**
* Set the title of the revision
*
+ * @deprecated: since 1.31, this is now a noop. Pass the Title to the constructor instead.
+ *
* @param Title $title
*/
public function setTitle( $title ) {
- $this->mTitle = $title;
+ if ( !$title->equals( $this->getTitle() ) ) {
+ throw new InvalidArgumentException(
+ $title->getPrefixedText()
+ . ' is not the same as '
+ . $this->mRecord->getPageAsLinkTarget()->__toString()
+ );
+ }
}
/**
* @return int|null
*/
public function getPage() {
- return $this->mPage;
+ return $this->mRecord->getPageId();
}
/**
* @return int
*/
public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
- if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
- return 0;
- } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
- return 0;
- } else {
- return $this->mUser;
+ global $wgUser;
+
+ if ( $audience === self::FOR_THIS_USER && !$user ) {
+ $user = $wgUser;
}
+
+ $user = $this->mRecord->getUser( $audience, $user );
+ return $user ? $user->getId() : 0;
}
/**
* @return string
*/
public function getUserText( $audience = self::FOR_PUBLIC, User $user = null ) {
- $this->loadMutableFields();
+ global $wgUser;
- if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
- return '';
- } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $user ) ) {
- return '';
- } else {
- if ( $this->mUserText === null ) {
- $this->mUserText = User::whoIs( $this->mUser ); // load on demand
- if ( $this->mUserText === false ) {
- # This shouldn't happen, but it can if the wiki was recovered
- # via importing revs and there is no user table entry yet.
- $this->mUserText = $this->mOrigUserText;
- }
- }
- return $this->mUserText;
+ if ( $audience === self::FOR_THIS_USER && !$user ) {
+ $user = $wgUser;
}
+
+ $user = $this->mRecord->getUser( $audience, $user );
+ return $user ? $user->getName() : '';
}
/**
* @return string
*/
function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
- if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
- return '';
- } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $user ) ) {
- return '';
- } else {
- return $this->mComment;
+ global $wgUser;
+
+ if ( $audience === self::FOR_THIS_USER && !$user ) {
+ $user = $wgUser;
}
+
+ $comment = $this->mRecord->getComment( $audience, $user );
+ return $comment === null ? null : $comment->text;
}
/**
* @return bool
*/
public function isMinor() {
- return (bool)$this->mMinorEdit;
+ return $this->mRecord->isMinor();
}
/**
* @return int Rcid of the unpatrolled row, zero if there isn't one
*/
public function isUnpatrolled() {
- if ( $this->mUnpatrolled !== null ) {
- return $this->mUnpatrolled;
- }
- $rc = $this->getRecentChange();
- if ( $rc && $rc->getAttribute( 'rc_patrolled' ) == 0 ) {
- $this->mUnpatrolled = $rc->getAttribute( 'rc_id' );
- } else {
- $this->mUnpatrolled = 0;
- }
- return $this->mUnpatrolled;
+ return self::getRevisionStore()->isUnpatrolled( $this->mRecord );
}
/**
* @return RecentChange|null
*/
public function getRecentChange( $flags = 0 ) {
- $dbr = wfGetDB( DB_REPLICA );
-
- list( $dbType, ) = DBAccessObjectUtils::getDBOptions( $flags );
-
- return RecentChange::newFromConds(
- [
- 'rc_user_text' => $this->getUserText( self::RAW ),
- 'rc_timestamp' => $dbr->timestamp( $this->getTimestamp() ),
- 'rc_this_oldid' => $this->getId()
- ],
- __METHOD__,
- $dbType
- );
+ return self::getRevisionStore()->getRecentChange( $this->mRecord, $flags );
}
/**
* @return bool
*/
public function isDeleted( $field ) {
- if ( $this->isCurrent() && $field === self::DELETED_TEXT ) {
- // Current revisions of pages cannot have the content hidden. Skipping this
- // check is very useful for Parser as it fetches templates using newKnownCurrent().
- // Calling getVisibility() in that case triggers a verification database query.
- return false; // no need to check
- }
-
- return ( $this->getVisibility() & $field ) == $field;
+ return $this->mRecord->isDeleted( $field );
}
/**
* @return int
*/
public function getVisibility() {
- $this->loadMutableFields();
-
- return (int)$this->mDeleted;
+ return $this->mRecord->getVisibility();
}
/**
* Fetch revision content if it's available to the specified audience.
* If the specified audience does not have the ability to view this
- * revision, null will be returned.
+ * revision, or the content could not be loaded, null will be returned.
*
* @param int $audience One of:
* Revision::FOR_PUBLIC to be displayed to all users
- * Revision::FOR_THIS_USER to be displayed to $wgUser
+ * Revision::FOR_THIS_USER to be displayed to $user
* Revision::RAW get the text regardless of permissions
* @param User $user User object to check for, only if FOR_THIS_USER is passed
* to the $audience parameter
* @return Content|null
*/
public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) {
- if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
- return null;
- } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
+ global $wgUser;
+
+ if ( $audience === self::FOR_THIS_USER && !$user ) {
+ $user = $wgUser;
+ }
+
+ try {
+ return $this->mRecord->getContent( 'main', $audience, $user );
+ }
+ catch ( RevisionAccessException $e ) {
return null;
- } else {
- return $this->getContentInternal();
}
}
* Get original serialized data (without checking view restrictions)
*
* @since 1.21
+ * @deprecated since 1.31, use BlobStore::getBlob instead.
+ *
* @return string
*/
public function getSerializedData() {
- if ( $this->mText === null ) {
- // Revision is immutable. Load on demand.
- $this->mText = $this->loadText();
- }
-
- return $this->mText;
+ $slot = $this->getMainSlotRaw();
+ return $slot->getContent()->serialize();
}
/**
- * Gets the content object for the revision (or null on failure).
- *
- * Note that for mutable Content objects, each call to this method will return a
- * fresh clone.
- *
- * @since 1.21
- * @return Content|null The Revision's content, or null on failure.
- */
- protected function getContentInternal() {
- if ( $this->mContent === null ) {
- $text = $this->getSerializedData();
-
- if ( $text !== null && $text !== false ) {
- // Unserialize content
- $handler = $this->getContentHandler();
- $format = $this->getContentFormat();
-
- $this->mContent = $handler->unserializeContent( $text, $format );
- }
- }
-
- // NOTE: copy() will return $this for immutable content objects
- return $this->mContent ? $this->mContent->copy() : null;
- }
-
- /**
- * Returns the content model for this revision.
+ * Returns the content model for the main slot of this revision.
*
* If no content model was stored in the database, the default content model for the title is
* used to determine the content model to use. If no title is know, CONTENT_MODEL_WIKITEXT
* is used as a last resort.
*
+ * @todo: drop this, with MCR, there no longer is a single model associated with a revision.
+ *
* @return string The content model id associated with this revision,
* see the CONTENT_MODEL_XXX constants.
*/
public function getContentModel() {
- if ( !$this->mContentModel ) {
- $title = $this->getTitle();
- if ( $title ) {
- $this->mContentModel = ContentHandler::getDefaultModelFor( $title );
- } else {
- $this->mContentModel = CONTENT_MODEL_WIKITEXT;
- }
-
- assert( !empty( $this->mContentModel ) );
- }
-
- return $this->mContentModel;
+ return $this->getMainSlotRaw()->getModel();
}
/**
- * Returns the content format for this revision.
+ * Returns the content format for the main slot of this revision.
*
* If no content format was stored in the database, the default format for this
* revision's content model is returned.
*
+ * @todo: drop this, the format is irrelevant to the revision!
+ *
* @return string The content format id associated with this revision,
* see the CONTENT_FORMAT_XXX constants.
*/
public function getContentFormat() {
- if ( !$this->mContentFormat ) {
- $handler = $this->getContentHandler();
- $this->mContentFormat = $handler->getDefaultFormat();
+ $format = $this->getMainSlotRaw()->getFormat();
- assert( !empty( $this->mContentFormat ) );
+ if ( $format === null ) {
+ // if no format was stored along with the blob, fall back to default format
+ $format = $this->getContentHandler()->getDefaultFormat();
}
- return $this->mContentFormat;
+ return $format;
}
/**
* @return ContentHandler
*/
public function getContentHandler() {
- if ( !$this->mContentHandler ) {
- $model = $this->getContentModel();
- $this->mContentHandler = ContentHandler::getForModelID( $model );
-
- $format = $this->getContentFormat();
-
- if ( !$this->mContentHandler->isSupportedFormat( $format ) ) {
- throw new MWException( "Oops, the content format $format is not supported for "
- . "this content model, $model" );
- }
- }
-
- return $this->mContentHandler;
+ return ContentHandler::getForModelID( $this->getContentModel() );
}
/**
* @return string
*/
public function getTimestamp() {
- return wfTimestamp( TS_MW, $this->mTimestamp );
+ return $this->mRecord->getTimestamp();
}
/**
* @return bool
*/
public function isCurrent() {
- return $this->mCurrent;
+ return ( $this->mRecord instanceof RevisionStoreRecord ) && $this->mRecord->isCurrent();
}
/**
* @return Revision|null
*/
public function getPrevious() {
- if ( $this->getTitle() ) {
- $prev = $this->getTitle()->getPreviousRevisionID( $this->getId() );
- if ( $prev ) {
- return self::newFromTitle( $this->getTitle(), $prev );
- }
- }
- return null;
+ $rec = self::getRevisionStore()->getPreviousRevision( $this->mRecord, $this->getTitle() );
+ return $rec === null
+ ? null
+ : new Revision( $rec, self::READ_NORMAL, $this->getTitle() );
}
/**
* @return Revision|null
*/
public function getNext() {
- if ( $this->getTitle() ) {
- $next = $this->getTitle()->getNextRevisionID( $this->getId() );
- if ( $next ) {
- return self::newFromTitle( $this->getTitle(), $next );
- }
- }
- return null;
- }
-
- /**
- * Get previous revision Id for this page_id
- * This is used to populate rev_parent_id on save
- *
- * @param IDatabase $db
- * @return int
- */
- private function getPreviousRevisionId( $db ) {
- if ( $this->mPage === null ) {
- return 0;
- }
- # Use page_latest if ID is not given
- if ( !$this->mId ) {
- $prevId = $db->selectField( 'page', 'page_latest',
- [ 'page_id' => $this->mPage ],
- __METHOD__ );
- } else {
- $prevId = $db->selectField( 'revision', 'rev_id',
- [ 'rev_page' => $this->mPage, 'rev_id < ' . $this->mId ],
- __METHOD__,
- [ 'ORDER BY' => 'rev_id DESC' ] );
- }
- return intval( $prevId );
+ $rec = self::getRevisionStore()->getNextRevision( $this->mRecord, $this->getTitle() );
+ return $rec === null
+ ? null
+ : new Revision( $rec, self::READ_NORMAL, $this->getTitle() );
}
/**
return false;
}
- // Use external methods for external objects, text in table is URL-only then
- if ( in_array( 'external', $flags ) ) {
- $url = $text;
- $parts = explode( '://', $url, 2 );
- if ( count( $parts ) == 1 || $parts[1] == '' ) {
- return false;
- }
+ $cacheKey = isset( $row->old_id ) ? ( 'tt:' . $row->old_id ) : null;
- if ( isset( $row->old_id ) && $wiki === false ) {
- // Make use of the wiki-local revision text cache
- $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
- // The cached value should be decompressed, so handle that and return here
- return $cache->getWithSetCallback(
- $cache->makeKey( 'revisiontext', 'textid', $row->old_id ),
- self::getCacheTTL( $cache ),
- function () use ( $url, $wiki, $flags ) {
- // No negative caching per Revision::loadText()
- $text = ExternalStore::fetchFromURL( $url, [ 'wiki' => $wiki ] );
-
- return self::decompressRevisionText( $text, $flags );
- },
- [ 'pcGroup' => self::TEXT_CACHE_GROUP, 'pcTTL' => $cache::TTL_PROC_LONG ]
- );
- } else {
- $text = ExternalStore::fetchFromURL( $url, [ 'wiki' => $wiki ] );
- }
- }
-
- return self::decompressRevisionText( $text, $flags );
+ return self::getBlobStore( $wiki )->expandBlob( $text, $flags, $cacheKey );
}
/**
* @return string
*/
public static function compressRevisionText( &$text ) {
- global $wgCompressRevisions;
- $flags = [];
-
- # Revisions not marked this way will be converted
- # on load if $wgLegacyCharset is set in the future.
- $flags[] = 'utf-8';
-
- if ( $wgCompressRevisions ) {
- if ( function_exists( 'gzdeflate' ) ) {
- $deflated = gzdeflate( $text );
-
- if ( $deflated === false ) {
- wfLogWarning( __METHOD__ . ': gzdeflate() failed' );
- } else {
- $text = $deflated;
- $flags[] = 'gzip';
- }
- } else {
- wfDebug( __METHOD__ . " -- no zlib support, not compressing\n" );
- }
- }
- return implode( ',', $flags );
+ return self::getBlobStore()->compressData( $text );
}
/**
* @return string|bool Decompressed text, or false on failure
*/
public static function decompressRevisionText( $text, $flags ) {
- global $wgLegacyEncoding, $wgContLang;
-
- if ( $text === false ) {
- // Text failed to be fetched; nothing to do
- return false;
- }
-
- if ( in_array( 'gzip', $flags ) ) {
- # Deal with optional compression of archived pages.
- # This can be done periodically via maintenance/compressOld.php, and
- # as pages are saved if $wgCompressRevisions is set.
- $text = gzinflate( $text );
-
- if ( $text === false ) {
- wfLogWarning( __METHOD__ . ': gzinflate() failed' );
- return false;
- }
- }
-
- if ( in_array( 'object', $flags ) ) {
- # Generic compressed storage
- $obj = unserialize( $text );
- if ( !is_object( $obj ) ) {
- // Invalid object
- return false;
- }
- $text = $obj->getText();
- }
-
- if ( $text !== false && $wgLegacyEncoding
- && !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags )
- ) {
- # Old revisions kept around in a legacy encoding?
- # Upconvert on demand.
- # ("utf8" checked for compatibility with some broken
- # conversion scripts 2008-12-30)
- $text = $wgContLang->iconv( $wgLegacyEncoding, 'UTF-8', $text );
- }
-
- return $text;
+ return self::getBlobStore()->decompressData( $text, $flags );
}
/**
* @return int The revision ID
*/
public function insertOn( $dbw ) {
- global $wgDefaultExternalStore, $wgContentHandlerUseDB;
-
- // We're inserting a new revision, so we have to use master anyway.
- // If it's a null revision, it may have references to rows that
- // are not in the replica yet (the text row).
- $this->mQueryFlags |= self::READ_LATEST;
-
- // Not allowed to have rev_page equal to 0, false, etc.
- if ( !$this->mPage ) {
- $title = $this->getTitle();
- if ( $title instanceof Title ) {
- $titleText = ' for page ' . $title->getPrefixedText();
- } else {
- $titleText = '';
- }
- throw new MWException( "Cannot insert revision$titleText: page ID must be nonzero" );
- }
-
- $this->checkContentModel();
-
- $data = $this->mText;
- $flags = self::compressRevisionText( $data );
-
- # Write to external storage if required
- if ( $wgDefaultExternalStore ) {
- // Store and get the URL
- $data = ExternalStore::insertToDefault( $data );
- if ( !$data ) {
- throw new MWException( "Unable to store text to external storage" );
- }
- if ( $flags ) {
- $flags .= ',';
- }
- $flags .= 'external';
- }
-
- # Record the text (or external storage URL) to the text table
- if ( $this->mTextId === null ) {
- $dbw->insert( 'text',
- [
- 'old_text' => $data,
- 'old_flags' => $flags,
- ], __METHOD__
- );
- $this->mTextId = $dbw->insertId();
- }
-
- if ( $this->mComment === null ) {
- $this->mComment = "";
- }
-
- # Record the edit in revisions
- $row = [
- 'rev_page' => $this->mPage,
- 'rev_text_id' => $this->mTextId,
- 'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
- 'rev_user' => $this->mUser,
- 'rev_user_text' => $this->mUserText,
- 'rev_timestamp' => $dbw->timestamp( $this->mTimestamp ),
- 'rev_deleted' => $this->mDeleted,
- 'rev_len' => $this->mSize,
- 'rev_parent_id' => $this->mParentId === null
- ? $this->getPreviousRevisionId( $dbw )
- : $this->mParentId,
- 'rev_sha1' => $this->mSha1 === null
- ? self::base36Sha1( $this->mText )
- : $this->mSha1,
- ];
- if ( $this->mId !== null ) {
- $row['rev_id'] = $this->mId;
- }
-
- list( $commentFields, $commentCallback ) =
- CommentStore::newKey( 'rev_comment' )->insertWithTempTable( $dbw, $this->mComment );
- $row += $commentFields;
-
- if ( $wgContentHandlerUseDB ) {
- // NOTE: Store null for the default model and format, to save space.
- // XXX: Makes the DB sensitive to changed defaults.
- // Make this behavior optional? Only in miser mode?
-
- $model = $this->getContentModel();
- $format = $this->getContentFormat();
+ global $wgUser;
- $title = $this->getTitle();
+ // Note that $this->mRecord->getId() will typically return null here, but not always,
+ // e.g. not when restoring a revision.
- if ( $title === null ) {
- throw new MWException( "Insufficient information to determine the title of the "
- . "revision's page!" );
+ if ( $this->mRecord->getUser( RevisionRecord::RAW ) === null ) {
+ if ( $this->mRecord instanceof MutableRevisionRecord ) {
+ $this->mRecord->setUser( $wgUser );
+ } else {
+ throw new MWException( 'Cannot insert revision with no associated user.' );
}
-
- $defaultModel = ContentHandler::getDefaultModelFor( $title );
- $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
-
- $row['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
- $row['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
- }
-
- $dbw->insert( 'revision', $row, __METHOD__ );
-
- if ( $this->mId === null ) {
- // Only if auto-increment was used
- $this->mId = $dbw->insertId();
}
- $commentCallback( $this->mId );
- // Assertion to try to catch T92046
- if ( (int)$this->mId === 0 ) {
- throw new UnexpectedValueException(
- 'After insert, Revision mId is ' . var_export( $this->mId, 1 ) . ': ' .
- var_export( $row, 1 )
- );
- }
+ $rec = self::getRevisionStore()->insertRevisionOn( $this->mRecord, $dbw );
- // Insert IP revision into ip_changes for use when querying for a range.
- if ( $this->mUser === 0 && IP::isValid( $this->mUserText ) ) {
- $ipcRow = [
- 'ipc_rev_id' => $this->mId,
- 'ipc_rev_timestamp' => $row['rev_timestamp'],
- 'ipc_hex' => IP::toHex( $row['rev_user_text'] ),
- ];
- $dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
- }
+ $this->mRecord = $rec;
// Avoid PHP 7.1 warning of passing $this by reference
$revision = $this;
- Hooks::run( 'RevisionInsertComplete', [ &$revision, $data, $flags ] );
+ // TODO: hard-deprecate in 1.32 (or even 1.31?)
+ Hooks::run( 'RevisionInsertComplete', [ &$revision, null, null ] );
- return $this->mId;
- }
-
- protected function checkContentModel() {
- global $wgContentHandlerUseDB;
-
- // Note: may return null for revisions that have not yet been inserted
- $title = $this->getTitle();
-
- $model = $this->getContentModel();
- $format = $this->getContentFormat();
- $handler = $this->getContentHandler();
-
- if ( !$handler->isSupportedFormat( $format ) ) {
- $t = $title->getPrefixedDBkey();
-
- throw new MWException( "Can't use format $format with content model $model on $t" );
- }
-
- if ( !$wgContentHandlerUseDB && $title ) {
- // if $wgContentHandlerUseDB is not set,
- // all revisions must use the default content model and format.
-
- $defaultModel = ContentHandler::getDefaultModelFor( $title );
- $defaultHandler = ContentHandler::getForModelID( $defaultModel );
- $defaultFormat = $defaultHandler->getDefaultFormat();
-
- if ( $this->getContentModel() != $defaultModel ) {
- $t = $title->getPrefixedDBkey();
-
- throw new MWException( "Can't save non-default content model with "
- . "\$wgContentHandlerUseDB disabled: model is $model, "
- . "default for $t is $defaultModel" );
- }
-
- if ( $this->getContentFormat() != $defaultFormat ) {
- $t = $title->getPrefixedDBkey();
-
- throw new MWException( "Can't use non-default content format with "
- . "\$wgContentHandlerUseDB disabled: format is $format, "
- . "default for $t is $defaultFormat" );
- }
- }
-
- $content = $this->getContent( self::RAW );
- $prefixedDBkey = $title->getPrefixedDBkey();
- $revId = $this->mId;
-
- if ( !$content ) {
- throw new MWException(
- "Content of revision $revId ($prefixedDBkey) could not be loaded for validation!"
- );
- }
- if ( !$content->isValid() ) {
- throw new MWException(
- "Content of revision $revId ($prefixedDBkey) is not valid! Content model is $model"
- );
- }
+ return $rec->getId();
}
/**
* @return string
*/
public static function base36Sha1( $text ) {
- return Wikimedia\base_convert( sha1( $text ), 16, 36, 31 );
- }
-
- /**
- * Get the text cache TTL
- *
- * @param WANObjectCache $cache
- * @return int
- */
- private static function getCacheTTL( WANObjectCache $cache ) {
- global $wgRevisionCacheExpiry;
-
- if ( $cache->getQoS( $cache::ATTR_EMULATION ) <= $cache::QOS_EMULATION_SQL ) {
- // Do not cache RDBMs blobs in...the RDBMs store
- $ttl = $cache::TTL_UNCACHEABLE;
- } else {
- $ttl = $wgRevisionCacheExpiry ?: $cache::TTL_UNCACHEABLE;
- }
-
- return $ttl;
- }
-
- /**
- * Lazy-load the revision's text.
- * Currently hardcoded to the 'text' table storage engine.
- *
- * @return string|bool The revision's text, or false on failure
- */
- private function loadText() {
- $cache = ObjectCache::getMainWANInstance();
-
- // No negative caching; negative hits on text rows may be due to corrupted replica DBs
- return $cache->getWithSetCallback(
- $cache->makeKey( 'revisiontext', 'textid', $this->getTextId() ),
- self::getCacheTTL( $cache ),
- function () {
- return $this->fetchText();
- },
- [ 'pcGroup' => self::TEXT_CACHE_GROUP, 'pcTTL' => $cache::TTL_PROC_LONG ]
- );
- }
-
- private function fetchText() {
- $textId = $this->getTextId();
-
- // If we kept data for lazy extraction, use it now...
- if ( $this->mTextRow !== null ) {
- $row = $this->mTextRow;
- $this->mTextRow = null;
- } else {
- $row = null;
- }
-
- // Callers doing updates will pass in READ_LATEST as usual. Since the text/blob tables
- // do not normally get rows changed around, set READ_LATEST_IMMUTABLE in those cases.
- $flags = $this->mQueryFlags;
- $flags |= DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST )
- ? self::READ_LATEST_IMMUTABLE
- : 0;
-
- list( $index, $options, $fallbackIndex, $fallbackOptions ) =
- DBAccessObjectUtils::getDBOptions( $flags );
-
- if ( !$row ) {
- // Text data is immutable; check replica DBs first.
- $row = wfGetDB( $index )->selectRow(
- 'text',
- [ 'old_text', 'old_flags' ],
- [ 'old_id' => $textId ],
- __METHOD__,
- $options
- );
- }
-
- // Fallback to DB_MASTER in some cases if the row was not found
- if ( !$row && $fallbackIndex !== null ) {
- // Use FOR UPDATE if it was used to fetch this revision. This avoids missing the row
- // due to REPEATABLE-READ. Also fallback to the master if READ_LATEST is provided.
- $row = wfGetDB( $fallbackIndex )->selectRow(
- 'text',
- [ 'old_text', 'old_flags' ],
- [ 'old_id' => $textId ],
- __METHOD__,
- $fallbackOptions
- );
- }
-
- if ( !$row ) {
- wfDebugLog( 'Revision', "No text row with ID '$textId' (revision {$this->getId()})." );
- }
-
- $text = self::getRevisionText( $row );
- if ( $row && $text === false ) {
- wfDebugLog( 'Revision', "No blob for text row '$textId' (revision {$this->getId()})." );
- }
-
- return is_string( $text ) ? $text : false;
+ return SlotRecord::base36Sha1( $text );
}
/**
* @return Revision|null Revision or null on error
*/
public static function newNullRevision( $dbw, $pageId, $summary, $minor, $user = null ) {
- global $wgContentHandlerUseDB;
-
- $fields = [ 'page_latest', 'page_namespace', 'page_title',
- 'rev_text_id', 'rev_len', 'rev_sha1' ];
-
- if ( $wgContentHandlerUseDB ) {
- $fields[] = 'rev_content_model';
- $fields[] = 'rev_content_format';
+ global $wgUser;
+ if ( !$user ) {
+ $user = $wgUser;
}
- $current = $dbw->selectRow(
- [ 'page', 'revision' ],
- $fields,
- [
- 'page_id' => $pageId,
- 'page_latest=rev_id',
- ],
- __METHOD__,
- [ 'FOR UPDATE' ] // T51581
- );
-
- if ( $current ) {
- if ( !$user ) {
- global $wgUser;
- $user = $wgUser;
- }
-
- $row = [
- 'page' => $pageId,
- 'user_text' => $user->getName(),
- 'user' => $user->getId(),
- 'comment' => $summary,
- 'minor_edit' => $minor,
- 'text_id' => $current->rev_text_id,
- 'parent_id' => $current->page_latest,
- 'len' => $current->rev_len,
- 'sha1' => $current->rev_sha1
- ];
-
- if ( $wgContentHandlerUseDB ) {
- $row['content_model'] = $current->rev_content_model;
- $row['content_format'] = $current->rev_content_format;
- }
+ $comment = CommentStoreComment::newUnsavedComment( $summary, null );
- $row['title'] = Title::makeTitle( $current->page_namespace, $current->page_title );
-
- $revision = new Revision( $row );
- } else {
- $revision = null;
- }
+ $title = Title::newFromID( $pageId );
+ $rec = self::getRevisionStore()->newNullRevision( $dbw, $title, $comment, $minor, $user );
- return $revision;
+ return new Revision( $rec );
}
/**
public static function userCanBitfield( $bitfield, $field, User $user = null,
Title $title = null
) {
- if ( $bitfield & $field ) { // aspect is deleted
- if ( $user === null ) {
- global $wgUser;
- $user = $wgUser;
- }
- if ( $bitfield & self::DELETED_RESTRICTED ) {
- $permissions = [ 'suppressrevision', 'viewsuppressed' ];
- } elseif ( $field & self::DELETED_TEXT ) {
- $permissions = [ 'deletedtext' ];
- } else {
- $permissions = [ 'deletedhistory' ];
- }
- $permissionlist = implode( ', ', $permissions );
- if ( $title === null ) {
- wfDebug( "Checking for $permissionlist due to $field match on $bitfield\n" );
- return call_user_func_array( [ $user, 'isAllowedAny' ], $permissions );
- } else {
- $text = $title->getPrefixedText();
- wfDebug( "Checking for $permissionlist on $text due to $field match on $bitfield\n" );
- foreach ( $permissions as $perm ) {
- if ( $title->userCan( $perm, $user ) ) {
- return true;
- }
- }
- return false;
- }
- } else {
- return true;
+ global $wgUser;
+
+ if ( !$user ) {
+ $user = $wgUser;
}
+
+ return RevisionRecord::userCanBitfield( $bitfield, $field, $user, $title );
}
/**
* @return string|bool False if not found
*/
static function getTimestampFromId( $title, $id, $flags = 0 ) {
- $db = ( $flags & self::READ_LATEST )
- ? wfGetDB( DB_MASTER )
- : wfGetDB( DB_REPLICA );
- // Casting fix for databases that can't take '' for rev_id
- if ( $id == '' ) {
- $id = 0;
- }
- $conds = [ 'rev_id' => $id ];
- $conds['rev_page'] = $title->getArticleID();
- $timestamp = $db->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ );
-
- return ( $timestamp !== false ) ? wfTimestamp( TS_MW, $timestamp ) : false;
+ return self::getRevisionStore()->getTimestampFromId( $title, $id, $flags );
}
/**
* @return int
*/
static function countByPageId( $db, $id ) {
- $row = $db->selectRow( 'revision', [ 'revCount' => 'COUNT(*)' ],
- [ 'rev_page' => $id ], __METHOD__ );
- if ( $row ) {
- return $row->revCount;
- }
- return 0;
+ return self::getRevisionStore()->countRevisionsByPageId( $db, $id );
}
/**
* @return int
*/
static function countByTitle( $db, $title ) {
- $id = $title->getArticleID();
- if ( $id ) {
- return self::countByPageId( $db, $id );
- }
- return 0;
+ return self::getRevisionStore()->countRevisionsByTitle( $db, $title );
}
/**
* @return bool True if the given user was the only one to edit since the given timestamp
*/
public static function userWasLastToEdit( $db, $pageId, $userId, $since ) {
- if ( !$userId ) {
- return false;
- }
-
if ( is_int( $db ) ) {
$db = wfGetDB( $db );
}
- $res = $db->select( 'revision',
- 'rev_user',
- [
- 'rev_page' => $pageId,
- 'rev_timestamp > ' . $db->addQuotes( $db->timestamp( $since ) )
- ],
- __METHOD__,
- [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ] );
- foreach ( $res as $row ) {
- if ( $row->rev_user != $userId ) {
- return false;
- }
- }
- return true;
+ return self::getRevisionStore()->userWasLastToEdit( $db, $pageId, $userId, $since );
}
/**
*
* This method allows for the use of caching, though accessing anything that normally
* requires permission checks (aside from the text) will trigger a small DB lookup.
- * The title will also be lazy loaded, though setTitle() can be used to preload it.
+ * The title will also be loaded if $pageIdOrTitle is an integer ID.
*
- * @param IDatabase $db
- * @param int $pageId Page ID
- * @param int $revId Known current revision of this page
+ * @param IDatabase $db ignored!
+ * @param int|Title $pageIdOrTitle Page ID or Title object
+ * @param int $revId Known current revision of this page. Determined automatically if not given.
* @return Revision|bool Returns false if missing
* @since 1.28
*/
- public static function newKnownCurrent( IDatabase $db, $pageId, $revId ) {
- $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
- return $cache->getWithSetCallback(
- // Page/rev IDs passed in from DB to reflect history merges
- $cache->makeGlobalKey( 'revision', $db->getDomainID(), $pageId, $revId ),
- $cache::TTL_WEEK,
- function ( $curValue, &$ttl, array &$setOpts ) use ( $db, $pageId, $revId ) {
- $setOpts += Database::getCacheSetOptions( $db );
-
- $rev = Revision::loadFromPageId( $db, $pageId, $revId );
- // Reflect revision deletion and user renames
- if ( $rev ) {
- $rev->mTitle = null; // mutable; lazy-load
- $rev->mRefreshMutableFields = true;
- }
-
- return $rev ?: false; // don't cache negatives
- }
- );
- }
-
- /**
- * For cached revisions, make sure the user name and rev_deleted is up-to-date
- */
- private function loadMutableFields() {
- if ( !$this->mRefreshMutableFields ) {
- return; // not needed
- }
+ public static function newKnownCurrent( IDatabase $db, $pageIdOrTitle, $revId = 0 ) {
+ $title = $pageIdOrTitle instanceof Title
+ ? $pageIdOrTitle
+ : Title::newFromID( $pageIdOrTitle );
- $this->mRefreshMutableFields = false;
- $dbr = wfGetLB( $this->mWiki )->getConnectionRef( DB_REPLICA, [], $this->mWiki );
- $row = $dbr->selectRow(
- [ 'revision', 'user' ],
- [ 'rev_deleted', 'user_name' ],
- [ 'rev_id' => $this->mId, 'user_id = rev_user' ],
- __METHOD__
- );
- if ( $row ) { // update values
- $this->mDeleted = (int)$row->rev_deleted;
- $this->mUserText = $row->user_name;
- }
+ $record = self::getRevisionStore()->getKnownCurrentRevision( $title, $revId );
+ return $record ? new Revision( $record ) : false;
}
}
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\MediaWikiServices;
use MediaWiki\Shell\CommandFactory;
+use MediaWiki\Storage\BlobStoreFactory;
+use MediaWiki\Storage\RevisionStore;
+use MediaWiki\Storage\SqlBlobStore;
return [
'DBLoadBalancerFactory' => function ( MediaWikiServices $services ) {
);
},
+ 'RevisionStore' => function ( MediaWikiServices $services ) {
+ /** @var SqlBlobStore $blobStore */
+ $blobStore = $services->getService( '_SqlBlobStore' );
+
+ $store = new RevisionStore(
+ $services->getDBLoadBalancer(),
+ $blobStore,
+ $services->getMainWANObjectCache()
+ );
+
+ $config = $services->getMainConfig();
+ $store->setContentHandlerUseDB( $config->get( 'ContentHandlerUseDB' ) );
+
+ return $store;
+ },
+
+ 'BlobStoreFactory' => function ( MediaWikiServices $services ) {
+ global $wgContLang;
+ return new BlobStoreFactory(
+ $services->getDBLoadBalancer(),
+ $services->getMainWANObjectCache(),
+ $services->getMainConfig(),
+ $wgContLang
+ );
+ },
+
+ 'BlobStore' => function ( MediaWikiServices $services ) {
+ return $services->getService( '_SqlBlobStore' );
+ },
+
+ '_SqlBlobStore' => function ( MediaWikiServices $services ) {
+ return $services->getBlobStoreFactory()->newSqlBlobStore();
+ },
+
///////////////////////////////////////////////////////////////////////////
// NOTE: When adding a service here, don't forget to add a getter function
// in the MediaWikiServices class. The convenience getter should just call
--- /dev/null
+<?php
+/**
+ * Exception representing a failure to look up a revision.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use RuntimeException;
+
+/**
+ * Exception representing a failure to access a data blob.
+ *
+ * @since 1.31
+ */
+class BlobAccessException extends RuntimeException {
+
+}
--- /dev/null
+<?php
+/**
+ * Service for loading and storing data blobs.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+/**
+ * Service for loading and storing data blobs.
+ *
+ * @note This was written to act as a drop-in replacement for the corresponding
+ * static methods in Revision.
+ *
+ * @since 1.31
+ */
+interface BlobStore {
+
+ /**
+ * Hint key for use with storeBlob, indicating the general role the block
+ * takes in the application. For instance, it should be "page-content" if
+ * the blob represents a Content object.
+ */
+ const DESIGNATION_HINT = 'designation';
+
+ /**
+ * Hint key for use with storeBlob, indicating the page the blob is associated with.
+ * This may be used for sharding.
+ */
+ const PAGE_HINT = 'page_id';
+
+ /**
+ * Hint key for use with storeBlob, indicating the slot the blob is associated with.
+ * May be relevant for reference counting.
+ */
+ const ROLE_HINT = 'role_name';
+
+ /**
+ * Hint key for use with storeBlob, indicating the revision the blob is associated with.
+ * This may be used for differential storage and reference counting.
+ */
+ const REVISION_HINT = 'rev_id';
+
+ /**
+ * Hint key for use with storeBlob, indicating the parent revision of the revision
+ * the blob is associated with. This may be used for differential storage.
+ */
+ const PARENT_HINT = 'rev_parent_id';
+
+ /**
+ * Hint key for use with storeBlob, providing the SHA1 hash of the blob as passed to the
+ * method. This can be used to avoid re-calculating the hash if it is needed by the BlobStore.
+ */
+ const SHA1_HINT = 'cont_sha1';
+
+ /**
+ * Hint key for use with storeBlob, indicating the model of the content encoded in the
+ * given blob. May be used to implement optimized storage for some well known models.
+ */
+ const MODEL_HINT = 'cont_model';
+
+ /**
+ * Hint key for use with storeBlob, indicating the serialization format used to create
+ * the blob, as a MIME type. May be used for optimized storage in the underlying database.
+ */
+ const FORMAT_HINT = 'cont_format';
+
+ /**
+ * Retrieve a blob, given an address.
+ *
+ * MCR migration note: this replaces Revision::loadText
+ *
+ * @param string $blobAddress The blob address as returned by storeBlob(),
+ * such as "tt:12345" or "ex:DB://s16/456/9876".
+ * @param int $queryFlags See IDBAccessObject.
+ *
+ * @throws BlobAccessException
+ * @return string binary blob data
+ */
+ public function getBlob( $blobAddress, $queryFlags = 0 );
+
+ /**
+ * Stores an arbitrary blob of data and returns an address that can be used with
+ * getBlob() to retrieve the same blob of data,
+ *
+ * @param string $data raw binary data
+ * @param array $hints An array of hints. Implementations may use the hints to optimize storage.
+ * All hints are optional, supported hints depend on the implementation. Hint names by
+ * convention correspond to the names of fields in the database. Callers are encouraged to
+ * provide the well known hints as defined by the XXX_HINT constants.
+ *
+ * @throws BlobAccessException
+ * @return string an address that can be used with getBlob() to retrieve the data.
+ */
+ public function storeBlob( $data, $hints = [] );
+
+}
--- /dev/null
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use Config;
+use Language;
+use WANObjectCache;
+use Wikimedia\Rdbms\LoadBalancer;
+
+/**
+ * Service for instantiating BlobStores
+ *
+ * This can be used to create BlobStore objects for other wikis.
+ *
+ * @since 1.31
+ */
+class BlobStoreFactory {
+
+ /**
+ * @var LoadBalancer
+ */
+ private $loadBalancer;
+
+ /**
+ * @var WANObjectCache
+ */
+ private $cache;
+
+ /**
+ * @var Config
+ */
+ private $config;
+
+ /**
+ * @var Language
+ */
+ private $contLang;
+
+ public function __construct(
+ LoadBalancer $loadBalancer,
+ WANObjectCache $cache,
+ Config $mainConfig,
+ Language $contLang
+ ) {
+ $this->loadBalancer = $loadBalancer;
+ $this->cache = $cache;
+ $this->config = $mainConfig;
+ $this->contLang = $contLang;
+ }
+
+ /**
+ * @since 1.31
+ *
+ * @param bool|string $wikiId The ID of the target wiki database. Use false for the local wiki.
+ *
+ * @return BlobStore
+ */
+ public function newBlobStore( $wikiId = false ) {
+ return $this->newSqlBlobStore( $wikiId );
+ }
+
+ /**
+ * @internal Please call newBlobStore and use the BlobStore interface.
+ *
+ * @param bool|string $wikiId The ID of the target wiki database. Use false for the local wiki.
+ *
+ * @return SqlBlobStore
+ */
+ public function newSqlBlobStore( $wikiId = false ) {
+ $store = new SqlBlobStore(
+ $this->loadBalancer,
+ $this->cache,
+ $wikiId
+ );
+
+ $store->setCompressBlobs( $this->config->get( 'CompressRevisions' ) );
+ $store->setCacheExpiry( $this->config->get( 'RevisionCacheExpiry' ) );
+ $store->setUseExternalStore( $this->config->get( 'DefaultExternalStore' ) !== false );
+
+ if ( $this->config->get( 'LegacyEncoding' ) ) {
+ $store->setLegacyEncoding( $this->config->get( 'LegacyEncoding' ), $this->contLang );
+ }
+
+ return $store;
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * Exception representing a failure to look up a revision.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+/**
+ * Exception throw when trying to access undefined fields on an incomplete RevisionRecord.
+ *
+ * @since 1.31
+ */
+class IncompleteRevisionException extends RevisionAccessException {
+
+}
--- /dev/null
+<?php
+/**
+ * Mutable RevisionRecord implementation, for building new revision entries programmatically.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use CommentStoreComment;
+use Content;
+use InvalidArgumentException;
+use MediaWiki\User\UserIdentity;
+use MWException;
+use Title;
+use Wikimedia\Assert\Assert;
+
+/**
+ * Mutable RevisionRecord implementation, for building new revision entries programmatically.
+ * Provides setters for all fields.
+ *
+ * @since 1.31
+ */
+class MutableRevisionRecord extends RevisionRecord {
+
+ /**
+ * Returns an incomplete MutableRevisionRecord which uses $parent as its
+ * parent revision, and inherits all slots form it. If saved unchanged,
+ * the new revision will act as a null-revision.
+ *
+ * @param RevisionRecord $parent
+ * @param CommentStoreComment $comment
+ * @param UserIdentity $user
+ * @param string $timestamp
+ *
+ * @return MutableRevisionRecord
+ */
+ public static function newFromParentRevision(
+ RevisionRecord $parent,
+ CommentStoreComment $comment,
+ UserIdentity $user,
+ $timestamp
+ ) {
+ // TODO: ideally, we wouldn't need a Title here
+ $title = Title::newFromLinkTarget( $parent->getPageAsLinkTarget() );
+ $rev = new MutableRevisionRecord( $title, $parent->getWikiId() );
+
+ $rev->setComment( $comment );
+ $rev->setUser( $user );
+ $rev->setTimestamp( $timestamp );
+
+ foreach ( $parent->getSlotRoles() as $role ) {
+ $slot = $parent->getSlot( $role, self::RAW );
+ $rev->inheritSlot( $slot );
+ }
+
+ $rev->setPageId( $parent->getPageId() );
+ $rev->setParentId( $parent->getId() );
+
+ return $rev;
+ }
+
+ /**
+ * @note Avoid calling this constructor directly. Use the appropriate methods
+ * in RevisionStore instead.
+ *
+ * @param Title $title The title of the page this Revision is associated with.
+ * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
+ * or false for the local site.
+ *
+ * @throws MWException
+ */
+ function __construct( Title $title, $wikiId = false ) {
+ $slots = new MutableRevisionSlots();
+
+ parent::__construct( $title, $slots, $wikiId );
+
+ $this->mSlots = $slots; // redundant, but nice for static analysis
+ }
+
+ /**
+ * @param int $parentId
+ */
+ public function setParentId( $parentId ) {
+ Assert::parameterType( 'integer', $parentId, '$parentId' );
+
+ $this->mParentId = $parentId;
+ }
+
+ /**
+ * Sets the given slot. If a slot with the same role is already present in the revision,
+ * it is replaced.
+ *
+ * @note This can only be used with a fresh "unattached" SlotRecord. Calling code that has a
+ * SlotRecord from another revision should use inheritSlot(). Calling code that has access to
+ * a Content object can use setContent().
+ *
+ * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+ *
+ * @note Calling this method will cause the revision size and hash to be re-calculated upon
+ * the next call to getSize() and getSha1(), respectively.
+ *
+ * @param SlotRecord $slot
+ */
+ public function setSlot( SlotRecord $slot ) {
+ if ( $slot->hasRevision() && $slot->getRevision() !== $this->getId() ) {
+ throw new InvalidArgumentException(
+ 'The given slot must be an unsaved, unattached one. '
+ . 'This slot is already attached to revision ' . $slot->getRevision() . '. '
+ . 'Use inheritSlot() instead to preserve a slot from a previous revision.'
+ );
+ }
+
+ $this->mSlots->setSlot( $slot );
+ $this->resetAggregateValues();
+ }
+
+ /**
+ * "Inherits" the given slot's content.
+ *
+ * If a slot with the same role is already present in the revision, it is replaced.
+ *
+ * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+ *
+ * @param SlotRecord $parentSlot
+ */
+ public function inheritSlot( SlotRecord $parentSlot ) {
+ $slot = SlotRecord::newInherited( $parentSlot );
+ $this->setSlot( $slot );
+ }
+
+ /**
+ * Sets the content for the slot with the given role.
+ *
+ * If a slot with the same role is already present in the revision, it is replaced.
+ * Calling code that has access to a SlotRecord can use inheritSlot() instead.
+ *
+ * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+ *
+ * @note Calling this method will cause the revision size and hash to be re-calculated upon
+ * the next call to getSize() and getSha1(), respectively.
+ *
+ * @param string $role
+ * @param Content $content
+ */
+ public function setContent( $role, Content $content ) {
+ $this->mSlots->setContent( $role, $content );
+ $this->resetAggregateValues();
+ }
+
+ /**
+ * Removes the slot with the given role from this revision.
+ * This effectively ends the "stream" with that role on the revision's page.
+ * Future revisions will no longer inherit this slot, unless it is added back explicitly.
+ *
+ * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+ *
+ * @note Calling this method will cause the revision size and hash to be re-calculated upon
+ * the next call to getSize() and getSha1(), respectively.
+ *
+ * @param string $role
+ */
+ public function removeSlot( $role ) {
+ $this->mSlots->removeSlot( $role );
+ $this->resetAggregateValues();
+ }
+
+ /**
+ * @param CommentStoreComment $comment
+ */
+ public function setComment( CommentStoreComment $comment ) {
+ $this->mComment = $comment;
+ }
+
+ /**
+ * Set revision hash, for optimization. Prevents getSha1() from re-calculating the hash.
+ *
+ * @note This should only be used if the calling code is sure that the given hash is correct
+ * for the revision's content, and there is no chance of the content being manipulated
+ * later. When in doubt, this method should not be called.
+ *
+ * @param string $sha1 SHA1 hash as a base36 string.
+ */
+ public function setSha1( $sha1 ) {
+ Assert::parameterType( 'string', $sha1, '$sha1' );
+
+ $this->mSha1 = $sha1;
+ }
+
+ /**
+ * Set nominal revision size, for optimization. Prevents getSize() from re-calculating the size.
+ *
+ * @note This should only be used if the calling code is sure that the given size is correct
+ * for the revision's content, and there is no chance of the content being manipulated
+ * later. When in doubt, this method should not be called.
+ *
+ * @param int $size nominal size in bogo-bytes
+ */
+ public function setSize( $size ) {
+ Assert::parameterType( 'integer', $size, '$size' );
+
+ $this->mSize = $size;
+ }
+
+ /**
+ * @param int $visibility
+ */
+ public function setVisibility( $visibility ) {
+ Assert::parameterType( 'integer', $visibility, '$visibility' );
+
+ $this->mDeleted = $visibility;
+ }
+
+ /**
+ * @param string $timestamp A timestamp understood by wfTimestamp
+ */
+ public function setTimestamp( $timestamp ) {
+ Assert::parameterType( 'string', $timestamp, '$timestamp' );
+
+ $this->mTimestamp = wfTimestamp( TS_MW, $timestamp );
+ }
+
+ /**
+ * @param bool $minorEdit
+ */
+ public function setMinorEdit( $minorEdit ) {
+ Assert::parameterType( 'boolean', $minorEdit, '$minorEdit' );
+
+ $this->mMinorEdit = $minorEdit;
+ }
+
+ /**
+ * Set the revision ID.
+ *
+ * MCR migration note: this replaces Revision::setId()
+ *
+ * @warning Use this with care, especially when preparing a revision for insertion
+ * into the database! The revision ID should only be fixed in special cases
+ * like preserving the original ID when restoring a revision.
+ *
+ * @param int $id
+ */
+ public function setId( $id ) {
+ Assert::parameterType( 'integer', $id, '$id' );
+
+ $this->mId = $id;
+ }
+
+ /**
+ * Sets the user identity associated with the revision
+ *
+ * @param UserIdentity $user
+ */
+ public function setUser( UserIdentity $user ) {
+ $this->mUser = $user;
+ }
+
+ /**
+ * @param int $pageId
+ */
+ public function setPageId( $pageId ) {
+ Assert::parameterType( 'integer', $pageId, '$pageId' );
+
+ if ( $this->mTitle->exists() && $pageId !== $this->mTitle->getArticleID() ) {
+ throw new InvalidArgumentException(
+ 'The given Title does not belong to page ID ' . $this->mPageId
+ );
+ }
+
+ $this->mPageId = $pageId;
+ }
+
+ /**
+ * Returns the nominal size of this revision.
+ *
+ * MCR migration note: this replaces Revision::getSize
+ *
+ * @return int The nominal size, may be computed on the fly if not yet known.
+ */
+ public function getSize() {
+ // If not known, re-calculate and remember. Will be reset when slots change.
+ if ( $this->mSize === null ) {
+ $this->mSize = $this->mSlots->computeSize();
+ }
+
+ return $this->mSize;
+ }
+
+ /**
+ * Returns the base36 sha1 of this revision.
+ *
+ * MCR migration note: this replaces Revision::getSha1
+ *
+ * @return string The revision hash, may be computed on the fly if not yet known.
+ */
+ public function getSha1() {
+ // If not known, re-calculate and remember. Will be reset when slots change.
+ if ( $this->mSha1 === null ) {
+ $this->mSha1 = $this->mSlots->computeSha1();
+ }
+
+ return $this->mSha1;
+ }
+
+ /**
+ * Invalidate cached aggregate values such as hash and size.
+ */
+ private function resetAggregateValues() {
+ $this->mSize = null;
+ $this->mSha1 = null;
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * Mutable version of RevisionSlots, for constructing a new revision.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use Content;
+
+/**
+ * Mutable version of RevisionSlots, for constructing a new revision.
+ *
+ * @since 1.31
+ */
+class MutableRevisionSlots extends RevisionSlots {
+
+ /**
+ * Constructs a MutableRevisionSlots that inherits from the given
+ * list of slots.
+ *
+ * @param SlotRecord[] $slots
+ *
+ * @return MutableRevisionSlots
+ */
+ public static function newFromParentRevisionSlots( array $slots ) {
+ $inherited = [];
+ foreach ( $slots as $slot ) {
+ $role = $slot->getRole();
+ $inherited[$role] = SlotRecord::newInherited( $slot );
+ }
+
+ return new MutableRevisionSlots( $inherited );
+ }
+
+ /**
+ * @param SlotRecord[] $slots An array of SlotRecords.
+ */
+ public function __construct( array $slots = [] ) {
+ parent::__construct( $slots );
+ }
+
+ /**
+ * Sets the given slot.
+ * If a slot with the same role is already present, it is replaced.
+ *
+ * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+ *
+ * @param SlotRecord $slot
+ */
+ public function setSlot( SlotRecord $slot ) {
+ if ( !is_array( $this->slots ) ) {
+ $this->getSlots(); // initialize $this->slots
+ }
+
+ $role = $slot->getRole();
+ $this->slots[$role] = $slot;
+ }
+
+ /**
+ * Sets the content for the slot with the given role.
+ * If a slot with the same role is already present, it is replaced.
+ *
+ * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+ *
+ * @param string $role
+ * @param Content $content
+ */
+ public function setContent( $role, Content $content ) {
+ $slot = SlotRecord::newUnsaved( $role, $content );
+ $this->setSlot( $slot );
+ }
+
+ /**
+ * Remove the slot for the given role, discontinue the corresponding stream.
+ *
+ * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+ *
+ * @param string $role
+ */
+ public function removeSlot( $role ) {
+ if ( !is_array( $this->slots ) ) {
+ $this->getSlots(); // initialize $this->slots
+ }
+
+ unset( $this->slots[$role] );
+ }
+
+ /**
+ * Return all slots that are not inherited.
+ *
+ * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+ *
+ * @return SlotRecord[]
+ */
+ public function getTouchedSlots() {
+ return array_filter(
+ $this->getSlots(),
+ function ( SlotRecord $slot ) {
+ return !$slot->isInherited();
+ }
+ );
+ }
+
+ /**
+ * Return all slots that are inherited.
+ *
+ * @note This may cause the slot meta-data for the revision to be lazy-loaded.
+ *
+ * @return SlotRecord[]
+ */
+ public function getInheritedSlots() {
+ return array_filter(
+ $this->getSlots(),
+ function ( SlotRecord $slot ) {
+ return $slot->isInherited();
+ }
+ );
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * Exception representing a failure to look up a revision.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use RuntimeException;
+
+/**
+ * Exception representing a failure to look up a revision.
+ *
+ * @since 1.31
+ */
+class RevisionAccessException extends RuntimeException {
+
+}
--- /dev/null
+<?php
+/**
+ * A RevisionRecord representing a revision of a deleted page persisted in the archive table.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use CommentStoreComment;
+use MediaWiki\User\UserIdentity;
+use Title;
+use User;
+use Wikimedia\Assert\Assert;
+
+/**
+ * A RevisionRecord representing a revision of a deleted page persisted in the archive table.
+ * Most getters on RevisionArchiveRecord will never return null. However, getId() and
+ * getParentId() may indeed return null if this information was not stored when the archive entry
+ * was created.
+ *
+ * @since 1.31
+ */
+class RevisionArchiveRecord extends RevisionRecord {
+
+ /**
+ * @var int
+ */
+ protected $mArchiveId;
+
+ /**
+ * @note Avoid calling this constructor directly. Use the appropriate methods
+ * in RevisionStore instead.
+ *
+ * @param Title $title The title of the page this Revision is associated with.
+ * @param UserIdentity $user
+ * @param CommentStoreComment $comment
+ * @param object $row An archive table row. Use RevisionStore::getArchiveQueryInfo() to build
+ * a query that yields the required fields.
+ * @param RevisionSlots $slots The slots of this revision.
+ * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
+ * or false for the local site.
+ */
+ function __construct(
+ Title $title,
+ UserIdentity $user,
+ CommentStoreComment $comment,
+ $row,
+ RevisionSlots $slots,
+ $wikiId = false
+ ) {
+ parent::__construct( $title, $slots, $wikiId );
+ Assert::parameterType( 'object', $row, '$row' );
+
+ $this->mArchiveId = intval( $row->ar_id );
+
+ // NOTE: ar_page_id may be different from $this->mTitle->getArticleID() in some cases,
+ // notably when a partially restored page has been moved, and a new page has been created
+ // with the same title. Archive rows for that title will then have the wrong page id.
+ $this->mPageId = isset( $row->ar_page_id ) ? intval( $row->ar_page_id ) : $title->getArticleID();
+
+ // NOTE: ar_parent_id = 0 indicates that there is no parent revision, while null
+ // indicates that the parent revision is unknown. As per MW 1.31, the database schema
+ // allows ar_parent_id to be NULL.
+ $this->mParentId = isset( $row->ar_parent_id ) ? intval( $row->ar_parent_id ) : null;
+ $this->mId = isset( $row->ar_rev_id ) ? intval( $row->ar_rev_id ) : null;
+ $this->mComment = $comment;
+ $this->mUser = $user;
+ $this->mTimestamp = wfTimestamp( TS_MW, $row->ar_timestamp );
+ $this->mMinorEdit = boolval( $row->ar_minor_edit );
+ $this->mDeleted = intval( $row->ar_deleted );
+ $this->mSize = intval( $row->ar_len );
+ $this->mSha1 = isset( $row->ar_sha1 ) ? $row->ar_sha1 : null;
+ }
+
+ /**
+ * Get archive row ID
+ *
+ * @return int
+ */
+ public function getArchiveId() {
+ return $this->mId;
+ }
+
+ /**
+ * @return int|null The revision id, or null if the original revision ID
+ * was not recorded in the archive table.
+ */
+ public function getId() {
+ // overwritten just to refine the contract specification.
+ return parent::getId();
+ }
+
+ /**
+ * @return int The nominal revision size, never null. May be computed on the fly.
+ */
+ public function getSize() {
+ // If length is null, calculate and remember it (potentially SLOW!).
+ // This is for compatibility with old database rows that don't have the field set.
+ if ( $this->mSize === null ) {
+ $this->mSize = $this->mSlots->computeSize();
+ }
+
+ return $this->mSize;
+ }
+
+ /**
+ * @return string The revision hash, never null. May be computed on the fly.
+ */
+ public function getSha1() {
+ // If hash is null, calculate it and remember (potentially SLOW!)
+ // This is for compatibility with old database rows that don't have the field set.
+ if ( $this->mSha1 === null ) {
+ $this->mSha1 = $this->mSlots->computeSha1();
+ }
+
+ return $this->mSha1;
+ }
+
+ /**
+ * @param int $audience
+ * @param User|null $user
+ *
+ * @return UserIdentity The identity of the revision author, null if access is forbidden.
+ */
+ public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
+ // overwritten just to add a guarantee to the contract
+ return parent::getUser( $audience, $user );
+ }
+
+ /**
+ * @param int $audience
+ * @param User|null $user
+ *
+ * @return CommentStoreComment The revision comment, null if access is forbidden.
+ */
+ public function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
+ // overwritten just to add a guarantee to the contract
+ return parent::getComment( $audience, $user );
+ }
+
+ /**
+ * @return string never null
+ */
+ public function getTimestamp() {
+ // overwritten just to add a guarantee to the contract
+ return parent::getTimestamp();
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * Service for constructing revision objects.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use MWException;
+use Title;
+
+/**
+ * Service for constructing revision objects.
+ *
+ * @since 1.31
+ *
+ * @note This was written to act as a drop-in replacement for the corresponding
+ * static methods in Revision.
+ */
+interface RevisionFactory {
+
+ /**
+ * Constructs a new RevisionRecord based on the given associative array following the MW1.29
+ * database convention for the Revision constructor.
+ *
+ * MCR migration note: this replaces Revision::newFromRow
+ *
+ * @deprecated since 1.31. Use a MutableRevisionRecord instead.
+ *
+ * @param array $fields
+ * @param int $queryFlags Flags for lazy loading behavior, see IDBAccessObject::READ_XXX.
+ * @param Title|null $title
+ *
+ * @return MutableRevisionRecord
+ * @throws MWException
+ */
+ public function newMutableRevisionFromArray( array $fields, $queryFlags = 0, Title $title = null );
+
+ /**
+ * Constructs a RevisionRecord given a database row and content slots.
+ *
+ * MCR migration note: this replaces Revision::newFromRow for rows based on the
+ * revision, slot, and content tables defined for MCR since MW1.31.
+ *
+ * @param object $row A query result row as a raw object.
+ * Use RevisionStore::getQueryInfo() to build a query that yields the required fields.
+ * @param int $queryFlags Flags for lazy loading behavior, see IDBAccessObject::READ_XXX.
+ * @param Title|null $title
+ *
+ * @return RevisionRecord
+ */
+ public function newRevisionFromRow( $row, $queryFlags = 0, Title $title = null );
+
+ /**
+ * Make a fake revision object from an archive table row. This is queried
+ * for permissions or even inserted (as in Special:Undelete)
+ *
+ * MCR migration note: this replaces Revision::newFromArchiveRow
+ *
+ * @param object $row A query result row as a raw object.
+ * Use RevisionStore::getArchiveQueryInfo() to build a query that yields the
+ * required fields.
+ * @param int $queryFlags Flags for lazy loading behavior, see IDBAccessObject::READ_XXX.
+ * @param Title $title
+ * @param array $overrides An associative array that allows fields in $row to be overwritten.
+ * Keys in this array correspond to field names in $row without the "ar_" prefix, so
+ * $overrides['user'] will override $row->ar_user, etc.
+ *
+ * @return RevisionRecord
+ */
+ public function newRevisionFromArchiveRow(
+ $row,
+ $queryFlags = 0,
+ Title $title = null,
+ array $overrides = []
+ );
+
+}
--- /dev/null
+<?php
+/**
+ * Service for looking up page revisions.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use \IDBAccessObject;
+use MediaWiki\Linker\LinkTarget;
+use Title;
+
+/**
+ * Service for looking up page revisions.
+ *
+ * @note This was written to act as a drop-in replacement for the corresponding
+ * static methods in Revision.
+ *
+ * @since 1.31
+ */
+interface RevisionLookup extends IDBAccessObject {
+
+ /**
+ * Load a page revision from a given revision ID number.
+ * Returns null if no such revision can be found.
+ *
+ * MCR migration note: this replaces Revision::newFromId
+ *
+ * $flags include:
+ *
+ * @param int $id
+ * @param int $flags bit field, see IDBAccessObject::READ_XXX
+ * @return RevisionRecord|null
+ */
+ public function getRevisionById( $id, $flags = 0 );
+
+ /**
+ * Load either the current, or a specified, revision
+ * that's attached to a given link target. If not attached
+ * to that link target, will return null.
+ *
+ * MCR migration note: this replaces Revision::newFromTitle
+ *
+ * @param LinkTarget $linkTarget
+ * @param int $revId (optional)
+ * @param int $flags bit field, see IDBAccessObject::READ_XXX
+ * @return RevisionRecord|null
+ */
+ public function getRevisionByTitle( LinkTarget $linkTarget, $revId = 0, $flags = 0 );
+
+ /**
+ * Load either the current, or a specified, revision
+ * that's attached to a given page ID.
+ * Returns null if no such revision can be found.
+ *
+ * MCR migration note: this replaces Revision::newFromPageId
+ *
+ * @param int $pageId
+ * @param int $revId (optional)
+ * @param int $flags bit field, see IDBAccessObject::READ_XXX
+ * @return RevisionRecord|null
+ */
+ public function getRevisionByPageId( $pageId, $revId = 0, $flags = 0 );
+
+ /**
+ * Get previous revision for this title
+ *
+ * MCR migration note: this replaces Revision::getPrevious
+ *
+ * @param RevisionRecord $rev
+ *
+ * @return RevisionRecord|null
+ */
+ public function getPreviousRevision( RevisionRecord $rev );
+
+ /**
+ * Get next revision for this title
+ *
+ * MCR migration note: this replaces Revision::getNext
+ *
+ * @param RevisionRecord $rev
+ *
+ * @return RevisionRecord|null
+ */
+ public function getNextRevision( RevisionRecord $rev );
+
+ /**
+ * Load a revision based on a known page ID and current revision ID from the DB
+ *
+ * This method allows for the use of caching, though accessing anything that normally
+ * requires permission checks (aside from the text) will trigger a small DB lookup.
+ *
+ * MCR migration note: this replaces Revision::newKnownCurrent
+ *
+ * @param Title $title the associated page title
+ * @param int $revId current revision of this page
+ *
+ * @return RevisionRecord|bool Returns false if missing
+ */
+ public function getKnownCurrentRevision( Title $title, $revId );
+
+}
--- /dev/null
+<?php
+/**
+ * Page revision base class.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use CommentStoreComment;
+use Content;
+use InvalidArgumentException;
+use LogicException;
+use MediaWiki\Linker\LinkTarget;
+use MediaWiki\User\UserIdentity;
+use MWException;
+use Title;
+use User;
+use Wikimedia\Assert\Assert;
+
+/**
+ * Page revision base class.
+ *
+ * RevisionRecords are considered value objects, but they may use callbacks for lazy loading.
+ * Note that while the base class has no setters, subclasses may offer a mutable interface.
+ *
+ * @since 1.31
+ */
+abstract class RevisionRecord {
+
+ // RevisionRecord deletion constants
+ const DELETED_TEXT = 1;
+ const DELETED_COMMENT = 2;
+ const DELETED_USER = 4;
+ const DELETED_RESTRICTED = 8;
+ const SUPPRESSED_USER = 12; // convenience
+ const SUPPRESSED_ALL = 15; // convenience
+
+ // Audience options for accessors
+ const FOR_PUBLIC = 1;
+ const FOR_THIS_USER = 2;
+ const RAW = 3;
+
+ /** @var string Wiki ID; false means the current wiki */
+ protected $mWiki = false;
+ /** @var int|null */
+ protected $mId;
+ /** @var int|null */
+ protected $mPageId;
+ /** @var UserIdentity|null */
+ protected $mUser;
+ /** @var bool */
+ protected $mMinorEdit = false;
+ /** @var string|null */
+ protected $mTimestamp;
+ /** @var int using the DELETED_XXX and SUPPRESSED_XXX flags */
+ protected $mDeleted = 0;
+ /** @var int|null */
+ protected $mSize;
+ /** @var string|null */
+ protected $mSha1;
+ /** @var int|null */
+ protected $mParentId;
+ /** @var CommentStoreComment|null */
+ protected $mComment;
+
+ /** @var Title */
+ protected $mTitle; // TODO: we only need the title for permission checks!
+
+ /** @var RevisionSlots */
+ protected $mSlots;
+
+ /**
+ * @note Avoid calling this constructor directly. Use the appropriate methods
+ * in RevisionStore instead.
+ *
+ * @param Title $title The title of the page this Revision is associated with.
+ * @param RevisionSlots $slots The slots of this revision.
+ * @param bool|string $wikiId the wiki ID of the site this Revision belongs to,
+ * or false for the local site.
+ *
+ * @throws MWException
+ */
+ function __construct( Title $title, RevisionSlots $slots, $wikiId = false ) {
+ Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
+
+ $this->mTitle = $title;
+ $this->mSlots = $slots;
+ $this->mWiki = $wikiId;
+
+ // XXX: this is a sensible default, but we may not have a Title object here in the future.
+ $this->mPageId = $title->getArticleID();
+ }
+
+ /**
+ * Implemented to defy serialization.
+ *
+ * @throws LogicException always
+ */
+ public function __sleep() {
+ throw new LogicException( __CLASS__ . ' is not serializable.' );
+ }
+
+ /**
+ * @param RevisionRecord $rec
+ *
+ * @return bool True if this RevisionRecord is known to have same content as $rec.
+ * False if the content is different (or not known to be the same).
+ */
+ public function hasSameContent( RevisionRecord $rec ) {
+ if ( $rec === $this ) {
+ return true;
+ }
+
+ if ( $this->getId() !== null && $this->getId() === $rec->getId() ) {
+ return true;
+ }
+
+ // check size before hash, since size is quicker to compute
+ if ( $this->getSize() !== $rec->getSize() ) {
+ return false;
+ }
+
+ // instead of checking the hash, we could also check the content addresses of all slots.
+
+ if ( $this->getSha1() === $rec->getSha1() ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the Content of the given slot of this revision.
+ * Call getSlotNames() to get a list of available slots.
+ *
+ * Note that for mutable Content objects, each call to this method will return a
+ * fresh clone.
+ *
+ * MCR migration note: this replaces Revision::getContent
+ *
+ * @param string $role The role name of the desired slot
+ * @param int $audience
+ * @param User|null $user
+ *
+ * @throws RevisionAccessException if the slot does not exist or slot data
+ * could not be lazy-loaded.
+ * @return Content|null The content of the given slot, or null if access is forbidden.
+ */
+ public function getContent( $role, $audience = self::FOR_PUBLIC, User $user = null ) {
+ // XXX: throwing an exception would be nicer, but would a further
+ // departure from the signature of Revision::getContent(), and thus
+ // more complex and error prone refactoring.
+ if ( !$this->audienceCan( self::DELETED_TEXT, $audience, $user ) ) {
+ return null;
+ }
+
+ $content = $this->getSlot( $role, $audience, $user )->getContent();
+ return $content->copy();
+ }
+
+ /**
+ * Returns meta-data for the given slot.
+ *
+ * @param string $role The role name of the desired slot
+ * @param int $audience
+ * @param User|null $user
+ *
+ * @throws RevisionAccessException if the slot does not exist or slot data
+ * could not be lazy-loaded.
+ * @return SlotRecord The slot meta-data. If access to the slot content is forbidden,
+ * calling getContent() on the SlotRecord will throw an exception.
+ */
+ public function getSlot( $role, $audience = self::FOR_PUBLIC, User $user = null ) {
+ $slot = $this->mSlots->getSlot( $role );
+
+ if ( !$this->audienceCan( self::DELETED_TEXT, $audience, $user ) ) {
+ return SlotRecord::newWithSuppressedContent( $slot );
+ }
+
+ return $slot;
+ }
+
+ /**
+ * Returns the slot names (roles) of all slots present in this revision.
+ * getContent() will succeed only for the names returned by this method.
+ *
+ * @return string[]
+ */
+ public function getSlotRoles() {
+ return $this->mSlots->getSlotRoles();
+ }
+
+ /**
+ * Get revision ID. Depending on the concrete subclass, this may return null if
+ * the revision ID is not known (e.g. because the revision does not yet exist
+ * in the database).
+ *
+ * MCR migration note: this replaces Revision::getId
+ *
+ * @return int|null
+ */
+ public function getId() {
+ return $this->mId;
+ }
+
+ /**
+ * Get parent revision ID (the original previous page revision).
+ * If there is no parent revision, this returns 0.
+ * If the parent revision is undefined or unknown, this returns null.
+ *
+ * @note As of MW 1.31, the database schema allows the parent ID to be
+ * NULL to indicate that it is unknown.
+ *
+ * MCR migration note: this replaces Revision::getParentId
+ *
+ * @return int|null
+ */
+ public function getParentId() {
+ return $this->mParentId;
+ }
+
+ /**
+ * Returns the nominal size of this revision, in bogo-bytes.
+ * May be calculated on the fly if not known, which may in the worst
+ * case may involve loading all content.
+ *
+ * MCR migration note: this replaces Revision::getSize
+ *
+ * @return int
+ */
+ abstract public function getSize();
+
+ /**
+ * Returns the base36 sha1 of this revision. This hash is derived from the
+ * hashes of all slots associated with the revision.
+ * May be calculated on the fly if not known, which may in the worst
+ * case may involve loading all content.
+ *
+ * MCR migration note: this replaces Revision::getSha1
+ *
+ * @return string
+ */
+ abstract public function getSha1();
+
+ /**
+ * Get the page ID. If the page does not yet exist, the page ID is 0.
+ *
+ * MCR migration note: this replaces Revision::getPage
+ *
+ * @return int
+ */
+ public function getPageId() {
+ return $this->mPageId;
+ }
+
+ /**
+ * Get the ID of the wiki this revision belongs to.
+ *
+ * @return string|false The wiki's logical name, of false to indicate the local wiki.
+ */
+ public function getWikiId() {
+ return $this->mWiki;
+ }
+
+ /**
+ * Returns the title of the page this revision is associated with as a LinkTarget object.
+ *
+ * MCR migration note: this replaces Revision::getTitle
+ *
+ * @return LinkTarget
+ */
+ public function getPageAsLinkTarget() {
+ return $this->mTitle;
+ }
+
+ /**
+ * Fetch revision's author's user identity, if it's available to the specified audience.
+ * If the specified audience does not have access to it, null will be
+ * returned. Depending on the concrete subclass, null may also be returned if the user is
+ * not yet specified.
+ *
+ * MCR migration note: this replaces Revision::getUser
+ *
+ * @param int $audience One of:
+ * RevisionRecord::FOR_PUBLIC to be displayed to all users
+ * RevisionRecord::FOR_THIS_USER to be displayed to the given user
+ * RevisionRecord::RAW get the ID regardless of permissions
+ * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
+ * to the $audience parameter
+ * @return UserIdentity|null
+ */
+ public function getUser( $audience = self::FOR_PUBLIC, User $user = null ) {
+ if ( !$this->audienceCan( self::DELETED_USER, $audience, $user ) ) {
+ return null;
+ } else {
+ return $this->mUser;
+ }
+ }
+
+ /**
+ * Fetch revision comment, if it's available to the specified audience.
+ * If the specified audience does not have access to the comment,
+ * this will return null. Depending on the concrete subclass, null may also be returned
+ * if the comment is not yet specified.
+ *
+ * MCR migration note: this replaces Revision::getComment
+ *
+ * @param int $audience One of:
+ * RevisionRecord::FOR_PUBLIC to be displayed to all users
+ * RevisionRecord::FOR_THIS_USER to be displayed to the given user
+ * RevisionRecord::RAW get the text regardless of permissions
+ * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
+ * to the $audience parameter
+ *
+ * @return CommentStoreComment|null
+ */
+ public function getComment( $audience = self::FOR_PUBLIC, User $user = null ) {
+ if ( !$this->audienceCan( self::DELETED_COMMENT, $audience, $user ) ) {
+ return null;
+ } else {
+ return $this->mComment;
+ }
+ }
+
+ /**
+ * MCR migration note: this replaces Revision::isMinor
+ *
+ * @return bool
+ */
+ public function isMinor() {
+ return (bool)$this->mMinorEdit;
+ }
+
+ /**
+ * MCR migration note: this replaces Revision::isDeleted
+ *
+ * @param int $field One of DELETED_* bitfield constants
+ *
+ * @return bool
+ */
+ public function isDeleted( $field ) {
+ return ( $this->getVisibility() & $field ) == $field;
+ }
+
+ /**
+ * Get the deletion bitfield of the revision
+ *
+ * MCR migration note: this replaces Revision::getVisibility
+ *
+ * @return int
+ */
+ public function getVisibility() {
+ return (int)$this->mDeleted;
+ }
+
+ /**
+ * MCR migration note: this replaces Revision::getTimestamp.
+ *
+ * May return null if the timestamp was not specified.
+ *
+ * @return string|null
+ */
+ public function getTimestamp() {
+ return $this->mTimestamp;
+ }
+
+ /**
+ * Check that the given audience has access to the given field.
+ *
+ * MCR migration note: this corresponds to Revision::userCan
+ *
+ * @param int $field One of self::DELETED_TEXT,
+ * self::DELETED_COMMENT,
+ * self::DELETED_USER
+ * @param int $audience One of:
+ * RevisionRecord::FOR_PUBLIC to be displayed to all users
+ * RevisionRecord::FOR_THIS_USER to be displayed to the given user
+ * RevisionRecord::RAW get the text regardless of permissions
+ * @param User|null $user User object to check. Required if $audience is FOR_THIS_USER,
+ * ignored otherwise.
+ *
+ * @return bool
+ */
+ protected function audienceCan( $field, $audience, User $user = null ) {
+ if ( $audience == self::FOR_PUBLIC && $this->isDeleted( $field ) ) {
+ return false;
+ } elseif ( $audience == self::FOR_THIS_USER ) {
+ if ( !$user ) {
+ throw new InvalidArgumentException(
+ 'A User object must be given when checking FOR_THIS_USER audience.'
+ );
+ }
+
+ if ( !$this->userCan( $field, $user ) ) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Determine if the current user is allowed to view a particular
+ * field of this revision, if it's marked as deleted.
+ *
+ * MCR migration note: this corresponds to Revision::userCan
+ *
+ * @param int $field One of self::DELETED_TEXT,
+ * self::DELETED_COMMENT,
+ * self::DELETED_USER
+ * @param User $user User object to check
+ * @return bool
+ */
+ protected function userCan( $field, User $user ) {
+ // TODO: use callback for permission checks, so we don't need to know a Title object!
+ return self::userCanBitfield( $this->getVisibility(), $field, $user, $this->mTitle );
+ }
+
+ /**
+ * Determine if the current user is allowed to view a particular
+ * field of this revision, if it's marked as deleted. This is used
+ * by various classes to avoid duplication.
+ *
+ * MCR migration note: this replaces Revision::userCanBitfield
+ *
+ * @param int $bitfield Current field
+ * @param int $field One of self::DELETED_TEXT = File::DELETED_FILE,
+ * self::DELETED_COMMENT = File::DELETED_COMMENT,
+ * self::DELETED_USER = File::DELETED_USER
+ * @param User $user User object to check
+ * @param Title|null $title A Title object to check for per-page restrictions on,
+ * instead of just plain userrights
+ * @return bool
+ */
+ public static function userCanBitfield( $bitfield, $field, User $user, Title $title = null ) {
+ if ( $bitfield & $field ) { // aspect is deleted
+ if ( $bitfield & self::DELETED_RESTRICTED ) {
+ $permissions = [ 'suppressrevision', 'viewsuppressed' ];
+ } elseif ( $field & self::DELETED_TEXT ) {
+ $permissions = [ 'deletedtext' ];
+ } else {
+ $permissions = [ 'deletedhistory' ];
+ }
+ $permissionlist = implode( ', ', $permissions );
+ if ( $title === null ) {
+ wfDebug( "Checking for $permissionlist due to $field match on $bitfield\n" );
+ return call_user_func_array( [ $user, 'isAllowedAny' ], $permissions );
+ } else {
+ $text = $title->getPrefixedText();
+ wfDebug( "Checking for $permissionlist on $text due to $field match on $bitfield\n" );
+ foreach ( $permissions as $perm ) {
+ if ( $title->userCan( $perm, $user ) ) {
+ return true;
+ }
+ }
+ return false;
+ }
+ } else {
+ return true;
+ }
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * Value object representing the set of slots belonging to a revision.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use Content;
+use LogicException;
+use Wikimedia\Assert\Assert;
+
+/**
+ * Value object representing the set of slots belonging to a revision.
+ *
+ * @since 1.31
+ */
+class RevisionSlots {
+
+ /** @var SlotRecord[]|callable */
+ protected $slots;
+
+ /**
+ * @param SlotRecord[]|callable $slots SlotRecords,
+ * or a callback that returns such a structure.
+ */
+ public function __construct( $slots ) {
+ Assert::parameterType( 'array|callable', $slots, '$slots' );
+
+ if ( is_callable( $slots ) ) {
+ $this->slots = $slots;
+ } else {
+ $this->setSlotsInternal( $slots );
+ }
+ }
+
+ /**
+ * @param SlotRecord[] $slots
+ */
+ private function setSlotsInternal( array $slots ) {
+ $this->slots = [];
+
+ // re-key the slot array
+ foreach ( $slots as $slot ) {
+ $role = $slot->getRole();
+ $this->slots[$role] = $slot;
+ }
+ }
+
+ /**
+ * Implemented to defy serialization.
+ *
+ * @throws LogicException always
+ */
+ public function __sleep() {
+ throw new LogicException( __CLASS__ . ' is not serializable.' );
+ }
+
+ /**
+ * Returns the Content of the given slot.
+ * Call getSlotNames() to get a list of available slots.
+ *
+ * Note that for mutable Content objects, each call to this method will return a
+ * fresh clone.
+ *
+ * @param string $role The role name of the desired slot
+ *
+ * @throws RevisionAccessException if the slot does not exist or slot data
+ * could not be lazy-loaded.
+ * @return Content
+ */
+ public function getContent( $role ) {
+ // Return a copy to be safe. Immutable content objects return $this from copy().
+ return $this->getSlot( $role )->getContent()->copy();
+ }
+
+ /**
+ * Returns the SlotRecord of the given slot.
+ * Call getSlotNames() to get a list of available slots.
+ *
+ * @param string $role The role name of the desired slot
+ *
+ * @throws RevisionAccessException if the slot does not exist or slot data
+ * could not be lazy-loaded.
+ * @return SlotRecord
+ */
+ public function getSlot( $role ) {
+ $slots = $this->getSlots();
+
+ if ( isset( $slots[$role] ) ) {
+ return $slots[$role];
+ } else {
+ throw new RevisionAccessException( 'No such slot: ' . $role );
+ }
+ }
+
+ /**
+ * Returns the slot names (roles) of all slots present in this revision.
+ * getContent() will succeed only for the names returned by this method.
+ *
+ * @return string[]
+ */
+ public function getSlotRoles() {
+ $slots = $this->getSlots();
+ return array_keys( $slots );
+ }
+
+ /**
+ * Computes the total nominal size of the revision's slots, in bogo-bytes.
+ *
+ * @warn This is potentially expensive! It may cause all slot's content to be loaded
+ * and deserialized.
+ *
+ * @return int
+ */
+ public function computeSize() {
+ return array_reduce( $this->getSlots(), function ( $accu, SlotRecord $slot ) {
+ return $accu + $slot->getSize();
+ }, 0 );
+ }
+
+ /**
+ * Returns an associative array that maps role names to SlotRecords. Each SlotRecord
+ * represents the content meta-data of a slot, together they define the content of
+ * a revision.
+ *
+ * @note This may cause the content meta-data for the revision to be lazy-loaded.
+ *
+ * @return SlotRecord[] revision slot/content rows, keyed by slot role name.
+ */
+ public function getSlots() {
+ if ( is_callable( $this->slots ) ) {
+ $slots = call_user_func( $this->slots );
+
+ Assert::postcondition(
+ is_array( $slots ),
+ 'Slots info callback should return an array of objects'
+ );
+
+ $this->setSlotsInternal( $slots );
+ }
+
+ return $this->slots;
+ }
+
+ /**
+ * Computes the combined hash of the revisions's slots.
+ *
+ * @note For backwards compatibility, the combined hash of a single slot
+ * is that slot's hash. For consistency, the combined hash of an empty set of slots
+ * is the hash of the empty string.
+ *
+ * @warn This is potentially expensive! It may cause all slot's content to be loaded
+ * and deserialized, then re-serialized and hashed.
+ *
+ * @return string
+ */
+ public function computeSha1() {
+ $slots = $this->getSlots();
+ ksort( $slots );
+
+ if ( empty( $slots ) ) {
+ return SlotRecord::base36Sha1( '' );
+ }
+
+ return array_reduce( $slots, function ( $accu, SlotRecord $slot ) {
+ return $accu === null
+ ? $slot->getSha1()
+ : SlotRecord::base36Sha1( $accu . $slot->getSha1() );
+ }, null );
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * Service for looking up page revisions.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * Attribution notice: when this file was created, much of its content was taken
+ * from the Revision.php file as present in release 1.30. Refer to the history
+ * of that file for original authorship.
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use CommentStore;
+use CommentStoreComment;
+use Content;
+use ContentHandler;
+use DBAccessObjectUtils;
+use Hooks;
+use \IDBAccessObject;
+use InvalidArgumentException;
+use IP;
+use LogicException;
+use MediaWiki\Linker\LinkTarget;
+use MediaWiki\User\UserIdentity;
+use MediaWiki\User\UserIdentityValue;
+use Message;
+use MWException;
+use MWUnknownContentModelException;
+use RecentChange;
+use stdClass;
+use Title;
+use User;
+use WANObjectCache;
+use Wikimedia\Assert\Assert;
+use Wikimedia\Rdbms\Database;
+use Wikimedia\Rdbms\DBConnRef;
+use Wikimedia\Rdbms\IDatabase;
+use Wikimedia\Rdbms\LoadBalancer;
+
+/**
+ * Service for looking up page revisions.
+ *
+ * @since 1.31
+ *
+ * @note This was written to act as a drop-in replacement for the corresponding
+ * static methods in Revision.
+ */
+class RevisionStore implements IDBAccessObject, RevisionFactory, RevisionLookup {
+
+ /**
+ * @var SqlBlobStore
+ */
+ private $blobStore;
+
+ /**
+ * @var bool|string
+ */
+ private $wikiId;
+
+ /**
+ * @var boolean
+ */
+ private $contentHandlerUseDB = true;
+
+ /**
+ * @var LoadBalancer
+ */
+ private $loadBalancer;
+
+ /**
+ * @var WANObjectCache
+ */
+ private $cache;
+
+ /**
+ * @todo $blobStore should be allowed to be any BlobStore!
+ *
+ * @param LoadBalancer $loadBalancer
+ * @param SqlBlobStore $blobStore
+ * @param WANObjectCache $cache
+ * @param bool|string $wikiId
+ */
+ public function __construct(
+ LoadBalancer $loadBalancer,
+ SqlBlobStore $blobStore,
+ WANObjectCache $cache,
+ $wikiId = false
+ ) {
+ Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
+
+ $this->loadBalancer = $loadBalancer;
+ $this->blobStore = $blobStore;
+ $this->cache = $cache;
+ $this->wikiId = $wikiId;
+ }
+
+ /**
+ * @return bool
+ */
+ public function getContentHandlerUseDB() {
+ return $this->contentHandlerUseDB;
+ }
+
+ /**
+ * @param bool $contentHandlerUseDB
+ */
+ public function setContentHandlerUseDB( $contentHandlerUseDB ) {
+ $this->contentHandlerUseDB = $contentHandlerUseDB;
+ }
+
+ /**
+ * @return LoadBalancer
+ */
+ private function getDBLoadBalancer() {
+ return $this->loadBalancer;
+ }
+
+ /**
+ * @param int $mode DB_MASTER or DB_REPLICA
+ *
+ * @return IDatabase
+ */
+ private function getDBConnection( $mode ) {
+ $lb = $this->getDBLoadBalancer();
+ return $lb->getConnection( $mode, [], $this->wikiId );
+ }
+
+ /**
+ * @param IDatabase $connection
+ */
+ private function releaseDBConnection( IDatabase $connection ) {
+ $lb = $this->getDBLoadBalancer();
+ $lb->reuseConnection( $connection );
+ }
+
+ /**
+ * @param int $mode DB_MASTER or DB_REPLICA
+ *
+ * @return DBConnRef
+ */
+ private function getDBConnectionRef( $mode ) {
+ $lb = $this->getDBLoadBalancer();
+ return $lb->getConnectionRef( $mode, [], $this->wikiId );
+ }
+
+ /**
+ * Determines the page Title based on the available information.
+ *
+ * MCR migration note: this corresponds to Revision::getTitle
+ *
+ * @param int|null $pageId
+ * @param int|null $revId
+ * @param int $queryFlags
+ *
+ * @return Title
+ * @throws RevisionAccessException
+ */
+ private function getTitle( $pageId, $revId, $queryFlags = 0 ) {
+ if ( !$pageId && !$revId ) {
+ throw new InvalidArgumentException( '$pageId and $revId cannot both be 0 or null' );
+ }
+
+ $title = null;
+
+ // Loading by ID is best, but Title::newFromID does not support that for foreign IDs.
+ if ( $pageId !== null && $pageId > 0 && $this->wikiId === false ) {
+ // TODO: better foreign title handling (introduce TitleFactory)
+ $title = Title::newFromID( $pageId, $queryFlags );
+ }
+
+ // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
+ if ( !$title && $revId !== null && $revId > 0 ) {
+ list( $dbMode, $dbOptions, , ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
+
+ $dbr = $this->getDbConnectionRef( $dbMode );
+ // @todo: Title::getSelectFields(), or Title::getQueryInfo(), or something like that
+ $row = $dbr->selectRow(
+ [ 'revision', 'page' ],
+ [
+ 'page_namespace',
+ 'page_title',
+ 'page_id',
+ 'page_latest',
+ 'page_is_redirect',
+ 'page_len',
+ ],
+ [ 'rev_id' => $revId ],
+ __METHOD__,
+ $dbOptions,
+ [ 'page' => [ 'JOIN', 'page_id=rev_page' ] ]
+ );
+ if ( $row ) {
+ // TODO: better foreign title handling (introduce TitleFactory)
+ $title = Title::newFromRow( $row );
+ }
+ }
+
+ if ( !$title ) {
+ throw new RevisionAccessException(
+ "Could not determine title for page ID $pageId and revision ID $revId"
+ );
+ }
+
+ return $title;
+ }
+
+ /**
+ * @param mixed $value
+ * @param string $name
+ *
+ * @throw IncompleteRevisionException if $value is null
+ * @return mixed $value, if $value is not null
+ */
+ private function failOnNull( $value, $name ) {
+ if ( $value === null ) {
+ throw new IncompleteRevisionException(
+ "$name must not be " . var_export( $value, true ) . "!"
+ );
+ }
+
+ return $value;
+ }
+
+ /**
+ * @param mixed $value
+ * @param string $name
+ *
+ * @throw IncompleteRevisionException if $value is empty
+ * @return mixed $value, if $value is not null
+ */
+ private function failOnEmpty( $value, $name ) {
+ if ( $value === null || $value === 0 || $value === '' ) {
+ throw new IncompleteRevisionException(
+ "$name must not be " . var_export( $value, true ) . "!"
+ );
+ }
+
+ return $value;
+ }
+
+ /**
+ * Insert a new revision into the database, returning the new revision ID
+ * number on success and dies horribly on failure.
+ *
+ * MCR migration note: this replaces Revision::insertOn
+ *
+ * @param RevisionRecord $rev
+ * @param IDatabase $dbw (master connection)
+ *
+ * @throws InvalidArgumentException
+ * @return RevisionRecord the new revision record.
+ */
+ public function insertRevisionOn( RevisionRecord $rev, IDatabase $dbw ) {
+ // TODO: pass in a DBTransactionContext instead of a database connection.
+ $this->checkDatabaseWikiId( $dbw );
+
+ if ( !$rev->getSlotRoles() ) {
+ throw new InvalidArgumentException( 'At least one slot needs to be defined!' );
+ }
+
+ if ( $rev->getSlotRoles() !== [ 'main' ] ) {
+ throw new InvalidArgumentException( 'Only the main slot is supported for now!' );
+ }
+
+ // TODO: we shouldn't need an actual Title here.
+ $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
+ $pageId = $this->failOnEmpty( $rev->getPageId(), 'rev_page field' ); // check this early
+
+ $parentId = $rev->getParentId() === null
+ ? $this->getPreviousRevisionId( $dbw, $rev )
+ : $rev->getParentId();
+
+ // Record the text (or external storage URL) to the blob store
+ $slot = $rev->getSlot( 'main', RevisionRecord::RAW );
+
+ $size = $this->failOnNull( $rev->getSize(), 'size field' );
+ $sha1 = $this->failOnEmpty( $rev->getSha1(), 'sha1 field' );
+
+ if ( !$slot->hasAddress() ) {
+ $content = $slot->getContent();
+ $format = $content->getDefaultFormat();
+ $model = $content->getModel();
+
+ $this->checkContentModel( $content, $title );
+
+ $data = $content->serialize( $format );
+
+ // Hints allow the blob store to optimize by "leaking" application level information to it.
+ // TODO: with the new MCR storage schema, we rev_id have this before storing the blobs.
+ // When we have it, add rev_id as a hint. Can be used with rev_parent_id for
+ // differential storage or compression of subsequent revisions.
+ $blobHints = [
+ BlobStore::DESIGNATION_HINT => 'page-content', // BlobStore may be used for other things too.
+ BlobStore::PAGE_HINT => $pageId,
+ BlobStore::ROLE_HINT => $slot->getRole(),
+ BlobStore::PARENT_HINT => $parentId,
+ BlobStore::SHA1_HINT => $slot->getSha1(),
+ BlobStore::MODEL_HINT => $model,
+ BlobStore::FORMAT_HINT => $format,
+ ];
+
+ $blobAddress = $this->blobStore->storeBlob( $data, $blobHints );
+ } else {
+ $blobAddress = $slot->getAddress();
+ $model = $slot->getModel();
+ $format = $slot->getFormat();
+ }
+
+ $textId = $this->blobStore->getTextIdFromAddress( $blobAddress );
+
+ if ( !$textId ) {
+ throw new LogicException(
+ 'Blob address not supported in 1.29 database schema: ' . $blobAddress
+ );
+ }
+
+ // getTextIdFromAddress() is free to insert something into the text table, so $textId
+ // may be a new value, not anything already contained in $blobAddress.
+ $blobAddress = 'tt:' . $textId;
+
+ $comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' );
+ $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
+ $timestamp = $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
+
+ # Record the edit in revisions
+ $row = [
+ 'rev_page' => $pageId,
+ 'rev_parent_id' => $parentId,
+ 'rev_text_id' => $textId,
+ 'rev_minor_edit' => $rev->isMinor() ? 1 : 0,
+ 'rev_user' => $this->failOnNull( $user->getId(), 'user field' ),
+ 'rev_user_text' => $this->failOnEmpty( $user->getName(), 'user_text field' ),
+ 'rev_timestamp' => $dbw->timestamp( $timestamp ),
+ 'rev_deleted' => $rev->getVisibility(),
+ 'rev_len' => $size,
+ 'rev_sha1' => $sha1,
+ ];
+
+ if ( $rev->getId() !== null ) {
+ // Needed to restore revisions with their original ID
+ $row['rev_id'] = $rev->getId();
+ }
+
+ list( $commentFields, $commentCallback ) =
+ CommentStore::newKey( 'rev_comment' )->insertWithTempTable( $dbw, $comment );
+ $row += $commentFields;
+
+ if ( $this->contentHandlerUseDB ) {
+ // MCR migration note: rev_content_model and rev_content_format will go away
+
+ $defaultModel = ContentHandler::getDefaultModelFor( $title );
+ $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
+
+ $row['rev_content_model'] = ( $model === $defaultModel ) ? null : $model;
+ $row['rev_content_format'] = ( $format === $defaultFormat ) ? null : $format;
+ }
+
+ $dbw->insert( 'revision', $row, __METHOD__ );
+
+ if ( !isset( $row['rev_id'] ) ) {
+ // only if auto-increment was used
+ $row['rev_id'] = intval( $dbw->insertId() );
+ }
+ $commentCallback( $row['rev_id'] );
+
+ // Insert IP revision into ip_changes for