Add namespace restrictions to `meta=siteinfo&siprop=namespaces` API result
[lhc/web/wiklou.git] / includes / Revision / RevisionRenderer.php
1 <?php
2 /**
3 * This file is part of MediaWiki.
4 *
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.
9 *
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.
14 *
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
19 *
20 * @file
21 */
22
23 namespace MediaWiki\Revision;
24
25 use Html;
26 use InvalidArgumentException;
27 use ParserOptions;
28 use ParserOutput;
29 use Psr\Log\LoggerInterface;
30 use Psr\Log\NullLogger;
31 use Title;
32 use User;
33 use Wikimedia\Rdbms\ILoadBalancer;
34
35 /**
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.
39 *
40 * One key responsibility of RevisionRenderer is implementing the layout used to combine
41 * the output of multiple slots.
42 *
43 * @since 1.32
44 */
45 class RevisionRenderer {
46
47 /** @var LoggerInterface */
48 private $saveParseLogger;
49
50 /** @var ILoadBalancer */
51 private $loadBalancer;
52
53 /** @var SlotRoleRegistry */
54 private $roleRegistery;
55
56 /** @var string|bool */
57 private $dbDomain;
58
59 /**
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
63 */
64 public function __construct(
65 ILoadBalancer $loadBalancer,
66 SlotRoleRegistry $roleRegistry,
67 $dbDomain = false
68 ) {
69 $this->loadBalancer = $loadBalancer;
70 $this->roleRegistery = $roleRegistry;
71 $this->dbDomain = $dbDomain;
72 $this->saveParseLogger = new NullLogger();
73 }
74
75 /**
76 * @param LoggerInterface $saveParseLogger
77 */
78 public function setLogger( LoggerInterface $saveParseLogger ) {
79 $this->saveParseLogger = $saveParseLogger;
80 }
81
82 /**
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 *
99 * @return RenderedRevision|null The rendered revision, or null if the audience checks fails.
100 */
101 public function getRenderedRevision(
102 RevisionRecord $rev,
103 ParserOptions $options = null,
104 User $forUser = null,
105 array $hints = []
106 ) {
107 if ( $rev->getWikiId() !== $this->dbDomain ) {
108 throw new InvalidArgumentException( 'Mismatching wiki ID ' . $rev->getWikiId() );
109 }
110
111 $audience = $hints['audience']
112 ?? ( $forUser ? RevisionRecord::FOR_THIS_USER : RevisionRecord::FOR_PUBLIC );
113
114 if ( !$rev->audienceCan( RevisionRecord::DELETED_TEXT, $audience, $forUser ) ) {
115 // Returning null here is awkward, but consist with the signature of
116 // Revision::getContent() and RevisionRecord::getContent().
117 return null;
118 }
119
120 if ( !$options ) {
121 $options = ParserOptions::newCanonical( $forUser ?: 'canonical' );
122 }
123
124 $useMaster = $hints['use-master'] ?? false;
125
126 $dbIndex = $useMaster
127 ? DB_MASTER // use latest values
128 : DB_REPLICA; // T154554
129
130 $options->setSpeculativeRevIdCallback( function () use ( $dbIndex ) {
131 return $this->getSpeculativeRevId( $dbIndex );
132 } );
133 $options->setSpeculativePageIdCallback( function () use ( $dbIndex ) {
134 return $this->getSpeculativePageId( $dbIndex );
135 } );
136
137 if ( !$rev->getId() && $rev->getTimestamp() ) {
138 // This is an unsaved revision with an already determined timestamp.
139 // Make the "current" time used during parsing match that of the revision.
140 // Any REVISION* parser variables will match up if the revision is saved.
141 $options->setTimestamp( $rev->getTimestamp() );
142 }
143
144 $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
145
146 $renderedRevision = new RenderedRevision(
147 $title,
148 $rev,
149 $options,
150 function ( RenderedRevision $rrev, array $hints ) {
151 return $this->combineSlotOutput( $rrev, $hints );
152 },
153 $audience,
154 $forUser
155 );
156
157 $renderedRevision->setSaveParseLogger( $this->saveParseLogger );
158
159 if ( isset( $hints['known-revision-output'] ) ) {
160 $renderedRevision->setRevisionParserOutput( $hints['known-revision-output'] );
161 }
162
163 return $renderedRevision;
164 }
165
166 private function getSpeculativeRevId( $dbIndex ) {
167 // Use a separate master connection in order to see the latest data, by avoiding
168 // stale data from REPEATABLE-READ snapshots.
169 $flags = ILoadBalancer::CONN_TRX_AUTOCOMMIT;
170
171 $db = $this->loadBalancer->getConnectionRef( $dbIndex, [], $this->dbDomain, $flags );
172
173 return 1 + (int)$db->selectField(
174 'revision',
175 'MAX(rev_id)',
176 [],
177 __METHOD__
178 );
179 }
180
181 private function getSpeculativePageId( $dbIndex ) {
182 // Use a separate master connection in order to see the latest data, by avoiding
183 // stale data from REPEATABLE-READ snapshots.
184 $flags = ILoadBalancer::CONN_TRX_AUTOCOMMIT;
185
186 $db = $this->loadBalancer->getConnectionRef( $dbIndex, [], $this->dbDomain, $flags );
187
188 return 1 + (int)$db->selectField(
189 'page',
190 'MAX(page_id)',
191 [],
192 __METHOD__
193 );
194 }
195
196 /**
197 * This implements the layout for combining the output of multiple slots.
198 *
199 * @todo Use placement hints from SlotRoleHandlers instead of hard-coding the layout.
200 *
201 * @param RenderedRevision $rrev
202 * @param array $hints see RenderedRevision::getRevisionParserOutput()
203 *
204 * @return ParserOutput
205 */
206 private function combineSlotOutput( RenderedRevision $rrev, array $hints = [] ) {
207 $revision = $rrev->getRevision();
208 $slots = $revision->getSlots()->getSlots();
209
210 $withHtml = $hints['generate-html'] ?? true;
211
212 // short circuit if there is only the main slot
213 if ( array_keys( $slots ) === [ SlotRecord::MAIN ] ) {
214 return $rrev->getSlotParserOutput( SlotRecord::MAIN );
215 }
216
217 // move main slot to front
218 if ( isset( $slots[SlotRecord::MAIN] ) ) {
219 $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
220 }
221
222 $combinedOutput = new ParserOutput( null );
223 $slotOutput = [];
224
225 $options = $rrev->getOptions();
226 $options->registerWatcher( [ $combinedOutput, 'recordOption' ] );
227
228 foreach ( $slots as $role => $slot ) {
229 $out = $rrev->getSlotParserOutput( $role, $hints );
230 $slotOutput[$role] = $out;
231
232 // XXX: should the SlotRoleHandler be able to intervene here?
233 $combinedOutput->mergeInternalMetaDataFrom( $out );
234 $combinedOutput->mergeTrackingMetaDataFrom( $out );
235 }
236
237 if ( $withHtml ) {
238 $html = '';
239 $first = true;
240 /** @var ParserOutput $out */
241 foreach ( $slotOutput as $role => $out ) {
242 $roleHandler = $this->roleRegistery->getRoleHandler( $role );
243
244 // TODO: put more fancy layout logic here, see T200915.
245 $layout = $roleHandler->getOutputLayoutHints();
246 $display = $layout['display'] ?? 'section';
247
248 if ( $display === 'none' ) {
249 continue;
250 }
251
252 if ( $first ) {
253 // skip header for the first slot
254 $first = false;
255 } else {
256 // NOTE: this placeholder is hydrated by ParserOutput::getText().
257 $headText = Html::element( 'mw:slotheader', [], $role );
258 $html .= Html::rawElement( 'h1', [ 'class' => 'mw-slot-header' ], $headText );
259 }
260
261 // XXX: do we want to put a wrapper div around the output?
262 // Do we want to let $roleHandler do that?
263 $html .= $out->getRawText();
264 $combinedOutput->mergeHtmlMetaDataFrom( $out );
265 }
266
267 $combinedOutput->setText( $html );
268 }
269
270 $options->registerWatcher( null );
271 return $combinedOutput;
272 }
273
274 }