3 * This file is part of MediaWiki.
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
23 namespace MediaWiki\Revision
;
26 use InvalidArgumentException
;
27 use MediaWiki\Storage\RevisionRecord
;
28 use MediaWiki\Storage\SlotRecord
;
31 use Psr\Log\LoggerInterface
;
32 use Psr\Log\NullLogger
;
35 use Wikimedia\Rdbms\ILoadBalancer
;
38 * The RevisionRenderer service provides access to rendered output for revisions.
39 * It does so be acting as a factory for RenderedRevision instances, which in turn
40 * provide lazy access to ParserOutput objects.
42 * One key responsibility of RevisionRenderer is implementing the layout used to combine
43 * the output of multiple slots.
47 class RevisionRenderer
{
49 /** @var LoggerInterface */
50 private $saveParseLogger;
52 /** @var ILoadBalancer */
53 private $loadBalancer;
55 /** @var string|bool */
59 * @param ILoadBalancer $loadBalancer
60 * @param bool|string $wikiId
62 public function __construct( ILoadBalancer
$loadBalancer, $wikiId = false ) {
63 $this->loadBalancer
= $loadBalancer;
64 $this->wikiId
= $wikiId;
66 $this->saveParseLogger
= new NullLogger();
70 * @param RevisionRecord $rev
71 * @param ParserOptions|null $options
72 * @param User|null $forUser User for privileged access. Default is unprivileged (public)
73 * access, unless the 'audience' hint is set to something else RevisionRecord::RAW.
74 * @param array $hints Hints given as an associative array. Known keys:
75 * - 'use-master' Use master when rendering for the parser cache during save.
76 * Default is to use a replica.
77 * - 'audience' the audience to use for content access. Default is
78 * RevisionRecord::FOR_PUBLIC if $forUser is not set, RevisionRecord::FOR_THIS_USER
79 * if $forUser is set. Can be set to RevisionRecord::RAW to disable audience checks.
81 * @return RenderedRevision|null The rendered revision, or null if the audience checks fails.
83 public function getRenderedRevision(
85 ParserOptions
$options = null,
89 if ( $rev->getWikiId() !== $this->wikiId
) {
90 throw new InvalidArgumentException( 'Mismatching wiki ID ' . $rev->getWikiId() );
93 $audience = $hints['audience']
94 ??
( $forUser ? RevisionRecord
::FOR_THIS_USER
: RevisionRecord
::FOR_PUBLIC
);
96 if ( !$rev->audienceCan( RevisionRecord
::DELETED_TEXT
, $audience, $forUser ) ) {
97 // Returning null here is awkward, but consist with the signature of
98 // Revision::getContent() and RevisionRecord::getContent().
103 $options = ParserOptions
::newCanonical( $forUser ?
: 'canonical' );
106 $useMaster = $hints['use-master'] ??
false;
108 $dbIndex = $useMaster
109 ? DB_MASTER
// use latest values
110 : DB_REPLICA
; // T154554
112 $options->setSpeculativeRevIdCallback( function () use ( $dbIndex ) {
113 return $this->getSpeculativeRevId( $dbIndex );
116 $title = Title
::newFromLinkTarget( $rev->getPageAsLinkTarget() );
118 $renderedRevision = new RenderedRevision(
122 function ( RenderedRevision
$rrev, array $hints ) {
123 return $this->combineSlotOutput( $rrev, $hints );
129 $renderedRevision->setSaveParseLogger( $this->saveParseLogger
);
131 return $renderedRevision;
134 private function getSpeculativeRevId( $dbIndex ) {
135 // Use a fresh master connection in order to see the latest data, by avoiding
136 // stale data from REPEATABLE-READ snapshots.
137 // HACK: But don't use a fresh connection in unit tests, since it would not have
138 // the fake tables. This should be handled by the LoadBalancer!
139 $flags = defined( 'MW_PHPUNIT_TEST' ) ||
$dbIndex === DB_REPLICA
140 ?
0 : ILoadBalancer
::CONN_TRX_AUTOCOMMIT
;
142 $db = $this->loadBalancer
->getConnectionRef( $dbIndex, [], $this->wikiId
, $flags );
144 return 1 +
(int)$db->selectField(
153 * This implements the layout for combining the output of multiple slots.
155 * @todo Use placement hints from SlotRoleHandlers instead of hard-coding the layout.
157 * @param RenderedRevision $rrev
158 * @param array $hints see RenderedRevision::getRevisionParserOutput()
160 * @return ParserOutput
162 private function combineSlotOutput( RenderedRevision
$rrev, array $hints = [] ) {
163 $revision = $rrev->getRevision();
164 $slots = $revision->getSlots()->getSlots();
166 $withHtml = $hints['generate-html'] ??
true;
168 // short circuit if there is only the main slot
169 if ( array_keys( $slots ) === [ SlotRecord
::MAIN
] ) {
170 return $rrev->getSlotParserOutput( SlotRecord
::MAIN
);
173 // TODO: put fancy layout logic here, see T200915.
175 // move main slot to front
176 if ( isset( $slots[SlotRecord
::MAIN
] ) ) {
177 $slots = [ SlotRecord
::MAIN
=> $slots[SlotRecord
::MAIN
] ] +
$slots;
180 $combinedOutput = new ParserOutput( null );
183 $options = $rrev->getOptions();
184 $options->registerWatcher( [ $combinedOutput, 'recordOption' ] );
186 foreach ( $slots as $role => $slot ) {
187 $out = $rrev->getSlotParserOutput( $role, $hints );
188 $slotOutput[$role] = $out;
190 $combinedOutput->mergeInternalMetaDataFrom( $out, $role );
191 $combinedOutput->mergeTrackingMetaDataFrom( $out );
197 /** @var ParserOutput $out */
198 foreach ( $slotOutput as $role => $out ) {
200 // skip header for the first slot
203 // NOTE: this placeholder is hydrated by ParserOutput::getText().
204 $headText = Html
::element( 'mw:slotheader', [], $role );
205 $html .= Html
::rawElement( 'h1', [ 'class' => 'mw-slot-header' ], $headText );
208 $html .= $out->getRawText();
209 $combinedOutput->mergeHtmlMetaDataFrom( $out );
212 $combinedOutput->setText( $html );
215 $options->registerWatcher( null );
216 return $combinedOutput;