Merge "RevisionStoreDbTestBase, remove redundant needsDB override"
[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 MediaWiki\Storage\RevisionRecord;
28 use ParserOptions;
29 use ParserOutput;
30 use Psr\Log\LoggerInterface;
31 use Psr\Log\NullLogger;
32 use Title;
33 use User;
34 use Wikimedia\Rdbms\ILoadBalancer;
35
36 /**
37 * The RevisionRenderer service provides access to rendered output for revisions.
38 * It does so be acting as a factory for RenderedRevision instances, which in turn
39 * provide lazy access to ParserOutput objects.
40 *
41 * One key responsibility of RevisionRenderer is implementing the layout used to combine
42 * the output of multiple slots.
43 *
44 * @since 1.32
45 */
46 class RevisionRenderer {
47
48 /** @var LoggerInterface */
49 private $saveParseLogger;
50
51 /** @var ILoadBalancer */
52 private $loadBalancer;
53
54 /** @var string|bool */
55 private $wikiId;
56
57 /**
58 * @param ILoadBalancer $loadBalancer
59 * @param bool|string $wikiId
60 */
61 public function __construct( ILoadBalancer $loadBalancer, $wikiId = false ) {
62 $this->loadBalancer = $loadBalancer;
63 $this->wikiId = $wikiId;
64
65 $this->saveParseLogger = new NullLogger();
66 }
67
68 /**
69 * @param RevisionRecord $rev
70 * @param ParserOptions|null $options
71 * @param User|null $forUser User for privileged access. Default is unprivileged (public)
72 * access, unless the 'audience' hint is set to something else RevisionRecord::RAW.
73 * @param array $hints Hints given as an associative array. Known keys:
74 * - 'use-master' Use master when rendering for the parser cache during save.
75 * Default is to use a replica.
76 * - 'audience' the audience to use for content access. Default is
77 * RevisionRecord::FOR_PUBLIC if $forUser is not set, RevisionRecord::FOR_THIS_USER
78 * if $forUser is set. Can be set to RevisionRecord::RAW to disable audience checks.
79 *
80 * @return RenderedRevision|null The rendered revision, or null if the audience checks fails.
81 */
82 public function getRenderedRevision(
83 RevisionRecord $rev,
84 ParserOptions $options = null,
85 User $forUser = null,
86 array $hints = []
87 ) {
88 if ( $rev->getWikiId() !== $this->wikiId ) {
89 throw new InvalidArgumentException( 'Mismatching wiki ID ' . $rev->getWikiId() );
90 }
91
92 $audience = $hints['audience']
93 ?? ( $forUser ? RevisionRecord::FOR_THIS_USER : RevisionRecord::FOR_PUBLIC );
94
95 if ( !$rev->audienceCan( RevisionRecord::DELETED_TEXT, $audience, $forUser ) ) {
96 // Returning null here is awkward, but consist with the signature of
97 // Revision::getContent() and RevisionRecord::getContent().
98 return null;
99 }
100
101 if ( !$options ) {
102 $options = ParserOptions::newCanonical( $forUser ?: 'canonical' );
103 }
104
105 $useMaster = $hints['use-master'] ?? false;
106
107 $dbIndex = $useMaster
108 ? DB_MASTER // use latest values
109 : DB_REPLICA; // T154554
110
111 $options->setSpeculativeRevIdCallback( function () use ( $dbIndex ) {
112 return $this->getSpeculativeRevId( $dbIndex );
113 } );
114
115 $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
116
117 $renderedRevision = new RenderedRevision(
118 $title,
119 $rev,
120 $options,
121 function ( RenderedRevision $rrev, array $hints ) {
122 return $this->combineSlotOutput( $rrev, $hints );
123 },
124 $audience,
125 $forUser
126 );
127
128 $renderedRevision->setSaveParseLogger( $this->saveParseLogger );
129
130 return $renderedRevision;
131 }
132
133 private function getSpeculativeRevId( $dbIndex ) {
134 // Use a fresh master connection in order to see the latest data, by avoiding
135 // stale data from REPEATABLE-READ snapshots.
136 // HACK: But don't use a fresh connection in unit tests, since it would not have
137 // the fake tables. This should be handled by the LoadBalancer!
138 $flags = defined( 'MW_PHPUNIT_TEST' ) || $dbIndex === DB_REPLICA
139 ? 0 : ILoadBalancer::CONN_TRX_AUTOCOMMIT;
140
141 $db = $this->loadBalancer->getConnectionRef( $dbIndex, [], $this->wikiId, $flags );
142
143 return 1 + (int)$db->selectField(
144 'revision',
145 'MAX(rev_id)',
146 [],
147 __METHOD__
148 );
149 }
150
151 /**
152 * This implements the layout for combining the output of multiple slots.
153 *
154 * @todo Use placement hints from SlotRoleHandlers instead of hard-coding the layout.
155 *
156 * @param RenderedRevision $rrev
157 * @param array $hints see RenderedRevision::getRevisionParserOutput()
158 *
159 * @return ParserOutput
160 */
161 private function combineSlotOutput( RenderedRevision $rrev, array $hints = [] ) {
162 $revision = $rrev->getRevision();
163 $slots = $revision->getSlots()->getSlots();
164
165 $withHtml = $hints['generate-html'] ?? true;
166
167 // short circuit if there is only the main slot
168 if ( array_keys( $slots ) === [ 'main' ] ) {
169 return $rrev->getSlotParserOutput( 'main' );
170 }
171
172 // TODO: put fancy layout logic here, see T200915.
173
174 // move main slot to front
175 if ( isset( $slots['main'] ) ) {
176 $slots = [ 'main' => $slots['main'] ] + $slots;
177 }
178
179 $combinedOutput = new ParserOutput( null );
180 $slotOutput = [];
181
182 $options = $rrev->getOptions();
183 $options->registerWatcher( [ $combinedOutput, 'recordOption' ] );
184
185 foreach ( $slots as $role => $slot ) {
186 $out = $rrev->getSlotParserOutput( $role, $hints );
187 $slotOutput[$role] = $out;
188
189 $combinedOutput->mergeInternalMetaDataFrom( $out, $role );
190 $combinedOutput->mergeTrackingMetaDataFrom( $out );
191 }
192
193 if ( $withHtml ) {
194 $html = '';
195 $first = true;
196 /** @var ParserOutput $out */
197 foreach ( $slotOutput as $role => $out ) {
198 if ( $first ) {
199 // skip header for the first slot
200 $first = false;
201 } else {
202 // NOTE: this placeholder is hydrated by ParserOutput::getText().
203 $headText = Html::element( 'mw:slotheader', [], $role );
204 $html .= Html::rawElement( 'h1', [ 'class' => 'mw-slot-header' ], $headText );
205 }
206
207 $html .= $out->getRawText();
208 $combinedOutput->mergeHtmlMetaDataFrom( $out );
209 }
210
211 $combinedOutput->setText( $html );
212 }
213
214 $options->registerWatcher( null );
215 return $combinedOutput;
216 }
217
218 }