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
;
29 use Psr\Log\LoggerInterface
;
30 use Psr\Log\NullLogger
;
33 use Wikimedia\Rdbms\ILoadBalancer
;
36 * The RevisionRenderer service provides access to rendered output for revisions.
37 * It does so be acting as a factory for RenderedRevision instances, which in turn
38 * provide lazy access to ParserOutput objects.
40 * One key responsibility of RevisionRenderer is implementing the layout used to combine
41 * the output of multiple slots.
45 class RevisionRenderer
{
47 /** @var LoggerInterface */
48 private $saveParseLogger;
50 /** @var ILoadBalancer */
51 private $loadBalancer;
53 /** @var SlotRoleRegistry */
54 private $roleRegistery;
56 /** @var string|bool */
60 * @param ILoadBalancer $loadBalancer
61 * @param SlotRoleRegistry $roleRegistry
62 * @param bool|string $dbDomain DB domain of the relevant wiki or false for the current one
64 public function __construct(
65 ILoadBalancer
$loadBalancer,
66 SlotRoleRegistry
$roleRegistry,
69 $this->loadBalancer
= $loadBalancer;
70 $this->roleRegistery
= $roleRegistry;
71 $this->dbDomain
= $dbDomain;
72 $this->saveParseLogger
= new NullLogger();
76 * @param LoggerInterface $saveParseLogger
78 public function setLogger( LoggerInterface
$saveParseLogger ) {
79 $this->saveParseLogger
= $saveParseLogger;
83 * @param RevisionRecord $rev
84 * @param ParserOptions|null $options
85 * @param User|null $forUser User for privileged access. Default is unprivileged (public)
86 * access, unless the 'audience' hint is set to something else RevisionRecord::RAW.
87 * @param array $hints Hints given as an associative array. Known keys:
88 * - 'use-master' Use master when rendering for the parser cache during save.
89 * Default is to use a replica.
90 * - 'audience' the audience to use for content access. Default is
91 * RevisionRecord::FOR_PUBLIC if $forUser is not set, RevisionRecord::FOR_THIS_USER
92 * if $forUser is set. Can be set to RevisionRecord::RAW to disable audience checks.
93 * - 'known-revision-output' a combined ParserOutput for the revision, perhaps from
94 * some cache. the caller is responsible for ensuring that the ParserOutput indeed
95 * matched the $rev and $options. This mechanism is intended as a temporary stop-gap,
96 * for the time until caches have been changed to store RenderedRevision states instead
97 * of ParserOutput objects.
98 * @phan-param array{use-master?:bool,audience?:int,known-revision-output?:ParserOutput} $hints
100 * @return RenderedRevision|null The rendered revision, or null if the audience checks fails.
102 public function getRenderedRevision(
104 ParserOptions
$options = null,
105 User
$forUser = null,
108 if ( $rev->getWikiId() !== $this->dbDomain
) {
109 throw new InvalidArgumentException( 'Mismatching wiki ID ' . $rev->getWikiId() );
112 $audience = $hints['audience']
113 ??
( $forUser ? RevisionRecord
::FOR_THIS_USER
: RevisionRecord
::FOR_PUBLIC
);
115 if ( !$rev->audienceCan( RevisionRecord
::DELETED_TEXT
, $audience, $forUser ) ) {
116 // Returning null here is awkward, but consist with the signature of
117 // Revision::getContent() and RevisionRecord::getContent().
122 $options = ParserOptions
::newCanonical( $forUser ?
: 'canonical' );
125 $useMaster = $hints['use-master'] ??
false;
127 $dbIndex = $useMaster
128 ? DB_MASTER
// use latest values
129 : DB_REPLICA
; // T154554
131 $options->setSpeculativeRevIdCallback( function () use ( $dbIndex ) {
132 return $this->getSpeculativeRevId( $dbIndex );
134 $options->setSpeculativePageIdCallback( function () use ( $dbIndex ) {
135 return $this->getSpeculativePageId( $dbIndex );
138 if ( !$rev->getId() && $rev->getTimestamp() ) {
139 // This is an unsaved revision with an already determined timestamp.
140 // Make the "current" time used during parsing match that of the revision.
141 // Any REVISION* parser variables will match up if the revision is saved.
142 $options->setTimestamp( $rev->getTimestamp() );
145 $title = Title
::newFromLinkTarget( $rev->getPageAsLinkTarget() );
147 $renderedRevision = new RenderedRevision(
151 function ( RenderedRevision
$rrev, array $hints ) {
152 return $this->combineSlotOutput( $rrev, $hints );
158 $renderedRevision->setSaveParseLogger( $this->saveParseLogger
);
160 if ( isset( $hints['known-revision-output'] ) ) {
161 $renderedRevision->setRevisionParserOutput( $hints['known-revision-output'] );
164 return $renderedRevision;
167 private function getSpeculativeRevId( $dbIndex ) {
168 // Use a separate master connection in order to see the latest data, by avoiding
169 // stale data from REPEATABLE-READ snapshots.
170 $flags = ILoadBalancer
::CONN_TRX_AUTOCOMMIT
;
172 $db = $this->loadBalancer
->getConnectionRef( $dbIndex, [], $this->dbDomain
, $flags );
174 return 1 +
(int)$db->selectField(
182 private function getSpeculativePageId( $dbIndex ) {
183 // Use a separate master connection in order to see the latest data, by avoiding
184 // stale data from REPEATABLE-READ snapshots.
185 $flags = ILoadBalancer
::CONN_TRX_AUTOCOMMIT
;
187 $db = $this->loadBalancer
->getConnectionRef( $dbIndex, [], $this->dbDomain
, $flags );
189 return 1 +
(int)$db->selectField(
198 * This implements the layout for combining the output of multiple slots.
200 * @todo Use placement hints from SlotRoleHandlers instead of hard-coding the layout.
202 * @param RenderedRevision $rrev
203 * @param array $hints see RenderedRevision::getRevisionParserOutput()
205 * @return ParserOutput
207 private function combineSlotOutput( RenderedRevision
$rrev, array $hints = [] ) {
208 $revision = $rrev->getRevision();
209 $slots = $revision->getSlots()->getSlots();
211 $withHtml = $hints['generate-html'] ??
true;
213 // short circuit if there is only the main slot
214 if ( array_keys( $slots ) === [ SlotRecord
::MAIN
] ) {
215 return $rrev->getSlotParserOutput( SlotRecord
::MAIN
);
218 // move main slot to front
219 if ( isset( $slots[SlotRecord
::MAIN
] ) ) {
220 $slots = [ SlotRecord
::MAIN
=> $slots[SlotRecord
::MAIN
] ] +
$slots;
223 $combinedOutput = new ParserOutput( null );
226 $options = $rrev->getOptions();
227 $options->registerWatcher( [ $combinedOutput, 'recordOption' ] );
229 foreach ( $slots as $role => $slot ) {
230 $out = $rrev->getSlotParserOutput( $role, $hints );
231 $slotOutput[$role] = $out;
233 // XXX: should the SlotRoleHandler be able to intervene here?
234 $combinedOutput->mergeInternalMetaDataFrom( $out );
235 $combinedOutput->mergeTrackingMetaDataFrom( $out );
241 /** @var ParserOutput $out */
242 foreach ( $slotOutput as $role => $out ) {
243 $roleHandler = $this->roleRegistery
->getRoleHandler( $role );
245 // TODO: put more fancy layout logic here, see T200915.
246 $layout = $roleHandler->getOutputLayoutHints();
247 $display = $layout['display'] ??
'section';
249 if ( $display === 'none' ) {
254 // skip header for the first slot
257 // NOTE: this placeholder is hydrated by ParserOutput::getText().
258 $headText = Html
::element( 'mw:slotheader', [], $role );
259 $html .= Html
::rawElement( 'h1', [ 'class' => 'mw-slot-header' ], $headText );
262 // XXX: do we want to put a wrapper div around the output?
263 // Do we want to let $roleHandler do that?
264 $html .= $out->getRawText();
265 $combinedOutput->mergeHtmlMetaDataFrom( $out );
268 $combinedOutput->setText( $html );
271 $options->registerWatcher( null );
272 return $combinedOutput;