MediaWiki  master
RevisionRenderer.php
Go to the documentation of this file.
1 <?php
23 namespace MediaWiki\Revision;
24 
25 use Html;
26 use InvalidArgumentException;
29 use ParserOptions;
30 use ParserOutput;
31 use Psr\Log\LoggerInterface;
32 use Psr\Log\NullLogger;
34 
46 
48  private $saveParseLogger;
49 
51  private $loadBalancer;
52 
54  private $roleRegistery;
55 
57  private $contentRenderer;
58 
60  private $dbDomain;
61 
68  public function __construct(
69  ILoadBalancer $loadBalancer,
70  SlotRoleRegistry $roleRegistry,
71  ContentRenderer $contentRenderer,
72  $dbDomain = false
73  ) {
74  $this->loadBalancer = $loadBalancer;
75  $this->roleRegistery = $roleRegistry;
76  $this->contentRenderer = $contentRenderer;
77  $this->dbDomain = $dbDomain;
78  $this->saveParseLogger = new NullLogger();
79  }
80 
84  public function setLogger( LoggerInterface $saveParseLogger ) {
85  $this->saveParseLogger = $saveParseLogger;
86  }
87 
110  public function getRenderedRevision(
111  RevisionRecord $rev,
112  ParserOptions $options = null,
113  Authority $forPerformer = null,
114  array $hints = []
115  ) {
116  if ( $rev->getWikiId() !== $this->dbDomain ) {
117  throw new InvalidArgumentException( 'Mismatching wiki ID ' . $rev->getWikiId() );
118  }
119 
120  $audience = $hints['audience']
122 
123  if ( !$rev->audienceCan( RevisionRecord::DELETED_TEXT, $audience, $forPerformer ) ) {
124  // Returning null here is awkward, but consistent with the signature of
125  // RevisionRecord::getContent().
126  return null;
127  }
128 
129  if ( !$options ) {
130  $options = $forPerformer ?
131  ParserOptions::newFromUser( $forPerformer->getUser() ) :
133  }
134 
135  if ( isset( $hints['causeAction'] ) ) {
136  $options->setRenderReason( $hints['causeAction'] );
137  }
138 
139  $usePrimary = $hints['use-master'] ?? false;
140 
141  $dbIndex = $usePrimary
142  ? DB_PRIMARY // use latest values
143  : DB_REPLICA; // T154554
144 
145  $options->setSpeculativeRevIdCallback( function () use ( $dbIndex ) {
146  return $this->getSpeculativeRevId( $dbIndex );
147  } );
148  $options->setSpeculativePageIdCallback( function () use ( $dbIndex ) {
149  return $this->getSpeculativePageId( $dbIndex );
150  } );
151 
152  if ( !$rev->getId() && $rev->getTimestamp() ) {
153  // This is an unsaved revision with an already determined timestamp.
154  // Make the "current" time used during parsing match that of the revision.
155  // Any REVISION* parser variables will match up if the revision is saved.
156  $options->setTimestamp( $rev->getTimestamp() );
157  }
158 
159  $renderedRevision = new RenderedRevision(
160  $rev,
161  $options,
162  $this->contentRenderer,
163  function ( RenderedRevision $rrev, array $hints ) {
164  return $this->combineSlotOutput( $rrev, $hints );
165  },
166  $audience,
167  $forPerformer
168  );
169 
170  $renderedRevision->setSaveParseLogger( $this->saveParseLogger );
171 
172  if ( isset( $hints['known-revision-output'] ) ) {
173  $renderedRevision->setRevisionParserOutput( $hints['known-revision-output'] );
174  }
175 
176  return $renderedRevision;
177  }
178 
179  private function getSpeculativeRevId( $dbIndex ) {
180  // Use a separate primary DB connection in order to see the latest data, by avoiding
181  // stale data from REPEATABLE-READ snapshots.
183 
184  $db = $this->loadBalancer->getConnectionRef( $dbIndex, [], $this->dbDomain, $flags );
185 
186  return 1 + (int)$db->selectField(
187  'revision',
188  'MAX(rev_id)',
189  [],
190  __METHOD__
191  );
192  }
193 
194  private function getSpeculativePageId( $dbIndex ) {
195  // Use a separate primary DB connection in order to see the latest data, by avoiding
196  // stale data from REPEATABLE-READ snapshots.
198 
199  $db = $this->loadBalancer->getConnectionRef( $dbIndex, [], $this->dbDomain, $flags );
200 
201  return 1 + (int)$db->selectField(
202  'page',
203  'MAX(page_id)',
204  [],
205  __METHOD__
206  );
207  }
208 
219  private function combineSlotOutput( RenderedRevision $rrev, array $hints = [] ) {
220  $revision = $rrev->getRevision();
221  $slots = $revision->getSlots()->getSlots();
222 
223  $withHtml = $hints['generate-html'] ?? true;
224 
225  // short circuit if there is only the main slot
226  if ( array_keys( $slots ) === [ SlotRecord::MAIN ] ) {
227  return $rrev->getSlotParserOutput( SlotRecord::MAIN, $hints );
228  }
229 
230  // move main slot to front
231  if ( isset( $slots[SlotRecord::MAIN] ) ) {
232  $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
233  }
234 
235  $combinedOutput = new ParserOutput( null );
236  $slotOutput = [];
237 
238  $options = $rrev->getOptions();
239  $options->registerWatcher( [ $combinedOutput, 'recordOption' ] );
240 
241  foreach ( $slots as $role => $slot ) {
242  $out = $rrev->getSlotParserOutput( $role, $hints );
243  $slotOutput[$role] = $out;
244 
245  // XXX: should the SlotRoleHandler be able to intervene here?
246  $combinedOutput->mergeInternalMetaDataFrom( $out );
247  $combinedOutput->mergeTrackingMetaDataFrom( $out );
248  }
249 
250  if ( $withHtml ) {
251  $html = '';
252  $first = true;
254  foreach ( $slotOutput as $role => $out ) {
255  $roleHandler = $this->roleRegistery->getRoleHandler( $role );
256 
257  // TODO: put more fancy layout logic here, see T200915.
258  $layout = $roleHandler->getOutputLayoutHints();
259  $display = $layout['display'] ?? 'section';
260 
261  if ( $display === 'none' ) {
262  continue;
263  }
264 
265  if ( $first ) {
266  // skip header for the first slot
267  $first = false;
268  } else {
269  // NOTE: this placeholder is hydrated by ParserOutput::getText().
270  $headText = Html::element( 'mw:slotheader', [], $role );
271  $html .= Html::rawElement( 'h1', [ 'class' => 'mw-slot-header' ], $headText );
272  }
273 
274  // XXX: do we want to put a wrapper div around the output?
275  // Do we want to let $roleHandler do that?
276  $html .= $out->getRawText();
277  $combinedOutput->mergeHtmlMetaDataFrom( $out );
278  }
279 
280  $combinedOutput->setText( $html );
281  }
282 
283  $options->registerWatcher( null );
284  return $combinedOutput;
285  }
286 
287 }
This class is a collection of static functions that serve two purposes:
Definition: Html.php:51
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:236
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:214
RenderedRevision represents the rendered representation of a revision.
setSaveParseLogger(LoggerInterface $saveParseLogger)
Page revision base class.
audienceCan( $field, $audience, Authority $performer=null)
Check that the given audience has access to the given field.
getWikiId()
Get the ID of the wiki this revision belongs to.
getTimestamp()
MCR migration note: this replaced Revision::getTimestamp.
getId( $wikiId=self::LOCAL)
Get revision ID.
The RevisionRenderer service provides access to rendered output for revisions.
setLogger(LoggerInterface $saveParseLogger)
__construct(ILoadBalancer $loadBalancer, SlotRoleRegistry $roleRegistry, ContentRenderer $contentRenderer, $dbDomain=false)
getRenderedRevision(RevisionRecord $rev, ParserOptions $options=null, Authority $forPerformer=null, array $hints=[])
A registry service for SlotRoleHandlers, used to define which slot roles are available on which page.
Set options of the Parser.
static newFromAnon()
Get a ParserOptions object for an anonymous user.
static newFromUser( $user)
Get a ParserOptions object from a given user.
This interface represents the authority associated the current execution context, such as a web reque...
Definition: Authority.php:37
This class is a delegate to ILBFactory for a given database cluster.
const CONN_TRX_AUTOCOMMIT
Yield a tracked autocommit-mode handle (reuse existing ones)
const DB_REPLICA
Definition: defines.php:26
const DB_PRIMARY
Definition: defines.php:28