During development a lot of classes were placed in MediaWiki\Storage\. The precedent set would mean that every class relating to something stored in a database table, plus all related value classes and such, would go into that namespace. Let's put them into MediaWiki\Revision\ instead. Then future classes related to the 'page' table can go into MediaWiki\Page\, future classes related to the 'user' table can go into MediaWiki\User\, and so on. Note I didn't move DerivedPageDataUpdater, PageUpdateException, PageUpdater, or RevisionSlotsUpdate in this patch. If these are kept long-term, they probably belong in MediaWiki\Page\ or MediaWiki\Edit\ instead. Bug: T204158 Change-Id: I16bea8927566a3c73c07e4f4afb3537e05aa04a5
217 lines
6.5 KiB
PHP
217 lines
6.5 KiB
PHP
<?php
|
|
/**
|
|
* This file is part of MediaWiki.
|
|
*
|
|
* 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\Revision;
|
|
|
|
use Html;
|
|
use InvalidArgumentException;
|
|
use ParserOptions;
|
|
use ParserOutput;
|
|
use Psr\Log\LoggerInterface;
|
|
use Psr\Log\NullLogger;
|
|
use Title;
|
|
use User;
|
|
use Wikimedia\Rdbms\ILoadBalancer;
|
|
|
|
/**
|
|
* The RevisionRenderer service provides access to rendered output for revisions.
|
|
* It does so be acting as a factory for RenderedRevision instances, which in turn
|
|
* provide lazy access to ParserOutput objects.
|
|
*
|
|
* One key responsibility of RevisionRenderer is implementing the layout used to combine
|
|
* the output of multiple slots.
|
|
*
|
|
* @since 1.32
|
|
*/
|
|
class RevisionRenderer {
|
|
|
|
/** @var LoggerInterface */
|
|
private $saveParseLogger;
|
|
|
|
/** @var ILoadBalancer */
|
|
private $loadBalancer;
|
|
|
|
/** @var string|bool */
|
|
private $wikiId;
|
|
|
|
/**
|
|
* @param ILoadBalancer $loadBalancer
|
|
* @param bool|string $wikiId
|
|
*/
|
|
public function __construct( ILoadBalancer $loadBalancer, $wikiId = false ) {
|
|
$this->loadBalancer = $loadBalancer;
|
|
$this->wikiId = $wikiId;
|
|
|
|
$this->saveParseLogger = new NullLogger();
|
|
}
|
|
|
|
/**
|
|
* @param RevisionRecord $rev
|
|
* @param ParserOptions|null $options
|
|
* @param User|null $forUser User for privileged access. Default is unprivileged (public)
|
|
* access, unless the 'audience' hint is set to something else RevisionRecord::RAW.
|
|
* @param array $hints Hints given as an associative array. Known keys:
|
|
* - 'use-master' Use master when rendering for the parser cache during save.
|
|
* Default is to use a replica.
|
|
* - 'audience' the audience to use for content access. Default is
|
|
* RevisionRecord::FOR_PUBLIC if $forUser is not set, RevisionRecord::FOR_THIS_USER
|
|
* if $forUser is set. Can be set to RevisionRecord::RAW to disable audience checks.
|
|
*
|
|
* @return RenderedRevision|null The rendered revision, or null if the audience checks fails.
|
|
*/
|
|
public function getRenderedRevision(
|
|
RevisionRecord $rev,
|
|
ParserOptions $options = null,
|
|
User $forUser = null,
|
|
array $hints = []
|
|
) {
|
|
if ( $rev->getWikiId() !== $this->wikiId ) {
|
|
throw new InvalidArgumentException( 'Mismatching wiki ID ' . $rev->getWikiId() );
|
|
}
|
|
|
|
$audience = $hints['audience']
|
|
?? ( $forUser ? RevisionRecord::FOR_THIS_USER : RevisionRecord::FOR_PUBLIC );
|
|
|
|
if ( !$rev->audienceCan( RevisionRecord::DELETED_TEXT, $audience, $forUser ) ) {
|
|
// Returning null here is awkward, but consist with the signature of
|
|
// Revision::getContent() and RevisionRecord::getContent().
|
|
return null;
|
|
}
|
|
|
|
if ( !$options ) {
|
|
$options = ParserOptions::newCanonical( $forUser ?: 'canonical' );
|
|
}
|
|
|
|
$useMaster = $hints['use-master'] ?? false;
|
|
|
|
$dbIndex = $useMaster
|
|
? DB_MASTER // use latest values
|
|
: DB_REPLICA; // T154554
|
|
|
|
$options->setSpeculativeRevIdCallback( function () use ( $dbIndex ) {
|
|
return $this->getSpeculativeRevId( $dbIndex );
|
|
} );
|
|
|
|
$title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
|
|
|
|
$renderedRevision = new RenderedRevision(
|
|
$title,
|
|
$rev,
|
|
$options,
|
|
function ( RenderedRevision $rrev, array $hints ) {
|
|
return $this->combineSlotOutput( $rrev, $hints );
|
|
},
|
|
$audience,
|
|
$forUser
|
|
);
|
|
|
|
$renderedRevision->setSaveParseLogger( $this->saveParseLogger );
|
|
|
|
return $renderedRevision;
|
|
}
|
|
|
|
private function getSpeculativeRevId( $dbIndex ) {
|
|
// Use a fresh master connection in order to see the latest data, by avoiding
|
|
// stale data from REPEATABLE-READ snapshots.
|
|
// HACK: But don't use a fresh connection in unit tests, since it would not have
|
|
// the fake tables. This should be handled by the LoadBalancer!
|
|
$flags = defined( 'MW_PHPUNIT_TEST' ) || $dbIndex === DB_REPLICA
|
|
? 0 : ILoadBalancer::CONN_TRX_AUTOCOMMIT;
|
|
|
|
$db = $this->loadBalancer->getConnectionRef( $dbIndex, [], $this->wikiId, $flags );
|
|
|
|
return 1 + (int)$db->selectField(
|
|
'revision',
|
|
'MAX(rev_id)',
|
|
[],
|
|
__METHOD__
|
|
);
|
|
}
|
|
|
|
/**
|
|
* This implements the layout for combining the output of multiple slots.
|
|
*
|
|
* @todo Use placement hints from SlotRoleHandlers instead of hard-coding the layout.
|
|
*
|
|
* @param RenderedRevision $rrev
|
|
* @param array $hints see RenderedRevision::getRevisionParserOutput()
|
|
*
|
|
* @return ParserOutput
|
|
*/
|
|
private function combineSlotOutput( RenderedRevision $rrev, array $hints = [] ) {
|
|
$revision = $rrev->getRevision();
|
|
$slots = $revision->getSlots()->getSlots();
|
|
|
|
$withHtml = $hints['generate-html'] ?? true;
|
|
|
|
// short circuit if there is only the main slot
|
|
if ( array_keys( $slots ) === [ SlotRecord::MAIN ] ) {
|
|
return $rrev->getSlotParserOutput( SlotRecord::MAIN );
|
|
}
|
|
|
|
// TODO: put fancy layout logic here, see T200915.
|
|
|
|
// move main slot to front
|
|
if ( isset( $slots[SlotRecord::MAIN] ) ) {
|
|
$slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
|
|
}
|
|
|
|
$combinedOutput = new ParserOutput( null );
|
|
$slotOutput = [];
|
|
|
|
$options = $rrev->getOptions();
|
|
$options->registerWatcher( [ $combinedOutput, 'recordOption' ] );
|
|
|
|
foreach ( $slots as $role => $slot ) {
|
|
$out = $rrev->getSlotParserOutput( $role, $hints );
|
|
$slotOutput[$role] = $out;
|
|
|
|
$combinedOutput->mergeInternalMetaDataFrom( $out, $role );
|
|
$combinedOutput->mergeTrackingMetaDataFrom( $out );
|
|
}
|
|
|
|
if ( $withHtml ) {
|
|
$html = '';
|
|
$first = true;
|
|
/** @var ParserOutput $out */
|
|
foreach ( $slotOutput as $role => $out ) {
|
|
if ( $first ) {
|
|
// skip header for the first slot
|
|
$first = false;
|
|
} else {
|
|
// NOTE: this placeholder is hydrated by ParserOutput::getText().
|
|
$headText = Html::element( 'mw:slotheader', [], $role );
|
|
$html .= Html::rawElement( 'h1', [ 'class' => 'mw-slot-header' ], $headText );
|
|
}
|
|
|
|
$html .= $out->getRawText();
|
|
$combinedOutput->mergeHtmlMetaDataFrom( $out );
|
|
}
|
|
|
|
$combinedOutput->setText( $html );
|
|
}
|
|
|
|
$options->registerWatcher( null );
|
|
return $combinedOutput;
|
|
}
|
|
|
|
}
|