MediaWiki master
RevisionRenderer.php
Go to the documentation of this file.
1<?php
9namespace MediaWiki\Revision;
10
11use InvalidArgumentException;
17use Psr\Log\LoggerAwareInterface;
18use Psr\Log\LoggerInterface;
19use Psr\Log\NullLogger;
21
32class RevisionRenderer implements LoggerAwareInterface {
33
35 private $saveParseLogger;
36
38 private $loadBalancer;
39
41 private $roleRegistry;
42
44 private $contentRenderer;
45
47 private $dbDomain;
48
55 public function __construct(
56 ILoadBalancer $loadBalancer,
57 SlotRoleRegistry $roleRegistry,
58 ContentRenderer $contentRenderer,
59 $dbDomain = false
60 ) {
61 $this->loadBalancer = $loadBalancer;
62 $this->roleRegistry = $roleRegistry;
63 $this->contentRenderer = $contentRenderer;
64 $this->dbDomain = $dbDomain;
65 $this->saveParseLogger = new NullLogger();
66 }
67
69 public function setLogger( LoggerInterface $saveParseLogger ): void {
70 $this->saveParseLogger = $saveParseLogger;
71 }
72
100 public function getRenderedRevision(
101 RevisionRecord $rev,
102 ?ParserOptions $options = null,
103 ?Authority $forPerformer = null,
104 array $hints = []
105 ) {
106 if ( $rev->getWikiId() !== $this->dbDomain ) {
107 throw new InvalidArgumentException(
108 "Mismatching wiki ID rev={$rev->getWikiId()}, this={$this->dbDomain}"
109 );
110 }
111
112 $audience = $hints['audience']
114
115 if ( !$rev->audienceCan( RevisionRecord::DELETED_TEXT, $audience, $forPerformer ) ) {
116 // Returning null here is awkward, but consistent with the signature of
117 // RevisionRecord::getContent().
118 return null;
119 }
120
121 if ( !$options ) {
122 $options = $forPerformer ?
123 ParserOptions::newFromUser( $forPerformer->getUser() ) :
124 ParserOptions::newFromAnon();
125 }
126
127 if ( isset( $hints['causeAction'] ) ) {
128 $options->setRenderReason( $hints['causeAction'] );
129 }
130
131 $usePrimary = $hints['use-master'] ?? false;
132
133 $dbIndex = $usePrimary
134 ? DB_PRIMARY // use latest values
135 : DB_REPLICA; // T154554
136
137 $options->setSpeculativeRevIdCallback( function () use ( $dbIndex ) {
138 return $this->getSpeculativeRevId( $dbIndex );
139 } );
140 $options->setSpeculativePageIdCallback( function () use ( $dbIndex ) {
141 return $this->getSpeculativePageId( $dbIndex );
142 } );
143
144 if ( !$rev->getId() && $rev->getTimestamp() ) {
145 // This is an unsaved revision with an already determined timestamp.
146 // Make the "current" time used during parsing match that of the revision.
147 // Any REVISION* parser variables will match up if the revision is saved.
148 $options->setTimestamp( $rev->getTimestamp() );
149 }
150
151 $previousOutput = $hints['previous-output'] ?? null;
152 $renderedRevision = new RenderedRevision(
153 $rev,
154 $options,
155 $this->contentRenderer,
156 function ( RenderedRevision $rrev, array $hints ) use ( $options, $previousOutput ) {
157 $h = [ 'previous-output' => $previousOutput ] + $hints;
158 return $this->combineSlotOutput( $rrev, $options, $h );
159 },
160 $audience,
161 $forPerformer
162 );
163
164 $renderedRevision->setSaveParseLogger( $this->saveParseLogger );
165
166 if ( isset( $hints['known-revision-output'] ) ) {
167 $renderedRevision->setRevisionParserOutput( $hints['known-revision-output'] );
168 }
169
170 return $renderedRevision;
171 }
172
173 private function getSpeculativeRevId( int $dbIndex ): int {
174 // Use a separate primary DB connection in order to see the latest data, by avoiding
175 // stale data from REPEATABLE-READ snapshots.
176 $flags = ILoadBalancer::CONN_TRX_AUTOCOMMIT;
177
178 $db = $this->loadBalancer->getConnection( $dbIndex, [], $this->dbDomain, $flags );
179
180 return 1 + (int)$db->newSelectQueryBuilder()
181 ->select( 'MAX(rev_id)' )
182 ->from( 'revision' )
183 ->caller( __METHOD__ )->fetchField();
184 }
185
186 private function getSpeculativePageId( int $dbIndex ): int {
187 // Use a separate primary DB connection in order to see the latest data, by avoiding
188 // stale data from REPEATABLE-READ snapshots.
189 $flags = ILoadBalancer::CONN_TRX_AUTOCOMMIT;
190
191 $db = $this->loadBalancer->getConnection( $dbIndex, [], $this->dbDomain, $flags );
192
193 return 1 + (int)$db->newSelectQueryBuilder()
194 ->select( 'MAX(page_id)' )
195 ->from( 'page' )
196 ->caller( __METHOD__ )->fetchField();
197 }
198
213 private function combineSlotOutput( RenderedRevision $rrev, ParserOptions $options, array $hints = [] ) {
214 $revision = $rrev->getRevision();
215 $slots = $revision->getSlots()->getSlots();
216
217 $withHtml = $hints['generate-html'] ?? true;
218 $previousOutputs = $this->splitSlotOutput( $rrev, $options, $hints['previous-output'] ?? null );
219
220 // short circuit if there is only the main slot
221 // T351026 hack: if use-parsoid is set, only return main slot output for now
222 // T351113 will remove this hack.
223 if ( array_keys( $slots ) === [ SlotRecord::MAIN ] || $options->getUseParsoid() ) {
224 $h = [ 'previous-output' => $previousOutputs[SlotRecord::MAIN] ] + $hints;
225 return $rrev->getSlotParserOutput( SlotRecord::MAIN, $h );
226 }
227
228 // move main slot to front
229 if ( isset( $slots[SlotRecord::MAIN] ) ) {
230 $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
231 }
232
233 $combinedOutput = new ParserOutput( null );
234 $slotOutput = [];
235
236 $options = $rrev->getOptions();
237 $options->registerWatcher( $combinedOutput->recordOption( ... ) );
238
239 foreach ( $slots as $role => $slot ) {
240 $h = [ 'previous-output' => $previousOutputs[$role] ] + $hints;
241 $out = $rrev->getSlotParserOutput( $role, $h );
242 $slotOutput[$role] = $out;
243
244 // XXX: should the SlotRoleHandler be able to intervene here?
245 $combinedOutput->mergeInternalMetaDataFrom( $out );
246 $combinedOutput->mergeTrackingMetaDataFrom( $out );
247 }
248
249 if ( $withHtml ) {
250 $html = '';
251 $first = true;
253 foreach ( $slotOutput as $role => $out ) {
254 $roleHandler = $this->roleRegistry->getRoleHandler( $role );
255
256 // TODO: put more fancy layout logic here, see T200915.
257 $layout = $roleHandler->getOutputLayoutHints();
258 $display = $layout['display'] ?? 'section';
259
260 if ( $display === 'none' ) {
261 continue;
262 }
263
264 if ( $first ) {
265 // skip header for the first slot
266 $first = false;
267 } else {
268 // NOTE: this placeholder is hydrated by ParserOutput::getText().
269 $headText = Html::element( 'mw:slotheader', [], $role );
270 $html .= Html::rawElement( 'h1', [ 'class' => 'mw-slot-header' ], $headText );
271 }
272
273 // XXX: do we want to put a wrapper div around the output?
274 // Do we want to let $roleHandler do that?
275 $html .= $out->getRawText();
276 $combinedOutput->mergeHtmlMetaDataFrom( $out );
277 }
278
279 $combinedOutput->setRawText( $html );
280 }
281
282 $options->registerWatcher( null );
283 return $combinedOutput;
284 }
285
303 private function splitSlotOutput( RenderedRevision $rrev, ParserOptions $options, ?ParserOutput $previousOutput ) {
304 // If there is no previous parse, then there is nothing to split.
305 $revision = $rrev->getRevision();
306 $revslots = $revision->getSlots();
307 if ( $previousOutput === null ) {
308 return array_fill_keys( $revslots->getSlotRoles(), null );
309 }
310
311 // short circuit if there is only the main slot
312 // T351026 hack: if use-parsoid is set, only return main slot output for now
313 // T351113 will remove this hack.
314 if ( $revslots->getSlotRoles() === [ SlotRecord::MAIN ] || $options->getUseParsoid() ) {
315 return [ SlotRecord::MAIN => $previousOutput ];
316 }
317
318 // @todo Currently slot combination is not reversible
319 return array_fill_keys( $revslots->getSlotRoles(), null );
320 }
321}
const DB_REPLICA
Definition defines.php:26
const DB_PRIMARY
Definition defines.php:28
This class is a collection of static functions that serve two purposes:
Definition Html.php:43
Set options of the Parser.
ParserOutput is a rendering of a Content object or a message.
RenderedRevision represents the rendered representation of a revision.
setSaveParseLogger(LoggerInterface $saveParseLogger)
Page revision base class.
getWikiId()
Get the ID of the wiki this revision belongs to.
getTimestamp()
MCR migration note: this replaced Revision::getTimestamp.
audienceCan( $field, $audience, ?Authority $performer=null)
Check that the given audience has access to the given field.
getId( $wikiId=self::LOCAL)
Get revision ID.
The RevisionRenderer service provides access to rendered output for revisions.
setLogger(LoggerInterface $saveParseLogger)
getRenderedRevision(RevisionRecord $rev, ?ParserOptions $options=null, ?Authority $forPerformer=null, array $hints=[])
__construct(ILoadBalancer $loadBalancer, SlotRoleRegistry $roleRegistry, ContentRenderer $contentRenderer, $dbDomain=false)
A registry service for SlotRoleHandlers, used to define which slot roles are available on which page.
This interface represents the authority associated with the current execution context,...
Definition Authority.php:23
This class is a delegate to ILBFactory for a given database cluster.
getConnection( $i, $groups=[], string|false $domain=false, $flags=0)
Get a lazy-connecting database handle for a specific or virtual (DB_PRIMARY/DB_REPLICA) server index.