Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
86.26% |
860 / 997 |
|
43.48% |
30 / 69 |
CRAP | |
0.00% |
0 / 1 |
DifferenceEngine | |
86.26% |
860 / 997 |
|
43.48% |
30 / 69 |
567.19 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
2 | |||
getSlotDiffRenderers | |
90.00% |
18 / 20 |
|
0.00% |
0 / 1 |
9.08 | |||
markAsSlotDiffRenderer | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getSlotContents | |
96.15% |
25 / 26 |
|
0.00% |
0 / 1 |
6 | |||
loadSingleSlot | |
50.00% |
3 / 6 |
|
0.00% |
0 / 1 |
4.12 | |||
addRevisionLoadError | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getRevisionLoadErrors | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasNewRevisionLoadError | |
50.00% |
2 / 4 |
|
0.00% |
0 / 1 |
4.12 | |||
getTitle | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
setReducedLineNumbers | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDiffLang | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getDefaultLanguage | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
wasCacheHit | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getOldid | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getNewid | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getOldRevision | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
getNewRevision | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
deletedLink | |
22.22% |
2 / 9 |
|
0.00% |
0 / 1 |
7.23 | |||
deletedIdMarker | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
showMissingRevision | |
91.67% |
11 / 12 |
|
0.00% |
0 / 1 |
6.02 | |||
hasDeletedRevision | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
4 | |||
getPermissionErrors | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
hasSuppressedRevision | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
5 | |||
getUserEditCount | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
2.00 | |||
getUserRoles | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
3.01 | |||
getUserMetaData | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
2.03 | |||
isUserAllowedToSeeRevisions | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
4 | |||
shouldBeHiddenFromUser | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
3 | |||
showDiffPage | |
93.36% |
197 / 211 |
|
0.00% |
0 / 1 |
43.54 | |||
showTablePrefixes | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
3 | |||
markPatrolledLink | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
4 | |||
getMarkPatrolledLinkInfo | |
95.45% |
21 / 22 |
|
0.00% |
0 / 1 |
8 | |||
revisionDeleteLink | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
2.01 | |||
renderNewRevision | |
76.47% |
39 / 51 |
|
0.00% |
0 / 1 |
12.58 | |||
showDiff | |
70.00% |
7 / 10 |
|
0.00% |
0 / 1 |
3.24 | |||
showDiffStyle | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
getDiff | |
81.82% |
9 / 11 |
|
0.00% |
0 / 1 |
4.10 | |||
incrementStats | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
getDiffBody | |
84.75% |
50 / 59 |
|
0.00% |
0 / 1 |
26.04 | |||
getDiffBodyForRole | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
30 | |||
getSlotHeader | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getSlotError | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getDiffBodyCacheKeyParams | |
92.86% |
13 / 14 |
|
0.00% |
0 / 1 |
5.01 | |||
getExtraCacheKeys | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
setSlotDiffOptions | |
66.67% |
6 / 9 |
|
0.00% |
0 / 1 |
5.93 | |||
setExtraQueryParams | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
generateContentDiffBody | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
generateTextDiffBody | |
71.43% |
5 / 7 |
|
0.00% |
0 / 1 |
2.09 | |||
getEngine | |
66.67% |
4 / 6 |
|
0.00% |
0 / 1 |
2.15 | |||
debug | |
22.22% |
2 / 9 |
|
0.00% |
0 / 1 |
7.23 | |||
getDebugString | |
66.67% |
4 / 6 |
|
0.00% |
0 / 1 |
3.33 | |||
localiseDiff | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
localiseLineNumbers | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getMultiNotice | |
95.92% |
47 / 49 |
|
0.00% |
0 / 1 |
21 | |||
intermediateEditsMsg | |
54.55% |
6 / 11 |
|
0.00% |
0 / 1 |
3.85 | |||
userCanEdit | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getRevisionHeader | |
100.00% |
47 / 47 |
|
100.00% |
1 / 1 |
7 | |||
addHeader | |
100.00% |
41 / 41 |
|
100.00% |
1 / 1 |
8 | |||
setContent | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
setRevisions | |
95.24% |
20 / 21 |
|
0.00% |
0 / 1 |
5 | |||
setTextLanguage | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
mapDiffPrevNext | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
7 | |||
loadRevisionIds | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
4.02 | |||
loadRevisionData | |
81.82% |
45 / 55 |
|
0.00% |
0 / 1 |
19.95 | |||
loadText | |
87.50% |
21 / 24 |
|
0.00% |
0 / 1 |
9.16 | |||
loadNewText | |
91.67% |
11 / 12 |
|
0.00% |
0 / 1 |
3.01 | |||
getTextDiffer | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
getSupportedFormats | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTextDiffFormat | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | /** |
3 | * User interface for the difference engine. |
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 | * @ingroup DifferenceEngine |
22 | */ |
23 | |
24 | use MediaWiki\CommentFormatter\CommentFormatter; |
25 | use MediaWiki\Content\Content; |
26 | use MediaWiki\Content\IContentHandlerFactory; |
27 | use MediaWiki\Context\ContextSource; |
28 | use MediaWiki\Context\IContextSource; |
29 | use MediaWiki\Debug\DeprecationHelper; |
30 | use MediaWiki\Diff\TextDiffer\ManifoldTextDiffer; |
31 | use MediaWiki\HookContainer\HookRunner; |
32 | use MediaWiki\Html\Html; |
33 | use MediaWiki\Language\Language; |
34 | use MediaWiki\Linker\Linker; |
35 | use MediaWiki\Linker\LinkRenderer; |
36 | use MediaWiki\MainConfigNames; |
37 | use MediaWiki\MediaWikiServices; |
38 | use MediaWiki\Message\Message; |
39 | use MediaWiki\Page\ParserOutputAccess; |
40 | use MediaWiki\Page\WikiPageFactory; |
41 | use MediaWiki\Permissions\Authority; |
42 | use MediaWiki\Permissions\PermissionStatus; |
43 | use MediaWiki\Revision\ArchivedRevisionLookup; |
44 | use MediaWiki\Revision\BadRevisionException; |
45 | use MediaWiki\Revision\RevisionRecord; |
46 | use MediaWiki\Revision\RevisionStore; |
47 | use MediaWiki\Revision\SlotRecord; |
48 | use MediaWiki\SpecialPage\SpecialPage; |
49 | use MediaWiki\Storage\NameTableAccessException; |
50 | use MediaWiki\Title\Title; |
51 | use MediaWiki\User\Options\UserOptionsLookup; |
52 | use MediaWiki\User\UserEditTracker; |
53 | use MediaWiki\User\UserGroupManager; |
54 | use MediaWiki\User\UserGroupMembership; |
55 | use MediaWiki\User\UserIdentity; |
56 | use MediaWiki\User\UserIdentityUtils; |
57 | use Wikimedia\Rdbms\IConnectionProvider; |
58 | |
59 | /** |
60 | * DifferenceEngine is responsible for rendering the difference between two revisions as HTML. |
61 | * This includes interpreting URL parameters, retrieving revision data, checking access permissions, |
62 | * selecting and invoking the diff generator class for the individual slots, doing post-processing |
63 | * on the generated diff, adding the rest of the HTML (such as headers) and writing the whole thing |
64 | * to OutputPage. |
65 | * |
66 | * DifferenceEngine can be subclassed by extensions, by customizing |
67 | * ContentHandler::createDifferenceEngine; the content handler will be selected based on the |
68 | * content model of the main slot (of the new revision, when the two are different). |
69 | * That might change after PageTypeHandler gets introduced. |
70 | * |
71 | * In the past, the class was also used for slot-level diff generation, and extensions might still |
72 | * subclass it and add such functionality. When that is the case (specifically, when a |
73 | * ContentHandler returns a standard SlotDiffRenderer but a nonstandard DifferenceEngine) |
74 | * DifferenceEngineSlotDiffRenderer will be used to convert the old behavior into the new one. |
75 | * |
76 | * @ingroup DifferenceEngine |
77 | * |
78 | * @todo This class is huge and poorly defined. It should be split into a controller responsible |
79 | * for interpreting query parameters, retrieving data and checking permissions; and a HTML renderer. |
80 | */ |
81 | class DifferenceEngine extends ContextSource { |
82 | |
83 | use DeprecationHelper; |
84 | |
85 | /** |
86 | * Constant to indicate diff cache compatibility. |
87 | * Bump this when changing the diff formatting in a way that |
88 | * fixes important bugs or such to force cached diff views to |
89 | * clear. |
90 | */ |
91 | private const DIFF_VERSION = '1.41'; |
92 | |
93 | /** |
94 | * Revision ID for the old revision. 0 for the revision previous to $mNewid, false |
95 | * if the diff does not have an old revision (e.g. 'oldid=<first revision of page>&diff=prev'), |
96 | * or the revision does not exist, null if the revision is unsaved. |
97 | * @var int|false|null |
98 | */ |
99 | protected $mOldid; |
100 | |
101 | /** |
102 | * Revision ID for the new revision. 0 for the last revision of the current page |
103 | * (as defined by the request context), false if the revision does not exist, null |
104 | * if it is unsaved, or an alias such as 'next'. |
105 | * @var int|string|false|null |
106 | */ |
107 | protected $mNewid; |
108 | |
109 | /** |
110 | * Old revision (left pane). |
111 | * Allowed to be an unsaved revision, unlikely that's ever needed though. |
112 | * False when the old revision does not exist; this can happen when using |
113 | * diff=prev on the first revision. Null when the revision should exist but |
114 | * doesn't (e.g. load failure); loadRevisionData() will return false in that |
115 | * case. Also null until lazy-loaded. Ignored completely when isContentOverridden |
116 | * is set. |
117 | * @var RevisionRecord|null|false |
118 | */ |
119 | private $mOldRevisionRecord; |
120 | |
121 | /** |
122 | * New revision (right pane). |
123 | * Note that this might be an unsaved revision (e.g. for edit preview). |
124 | * Null in case of load failure; diff methods will just return an error message in that case, |
125 | * and loadRevisionData() will return false. Also null until lazy-loaded. Ignored completely |
126 | * when isContentOverridden is set. |
127 | * @var RevisionRecord|null |
128 | */ |
129 | private $mNewRevisionRecord; |
130 | |
131 | /** |
132 | * Title of old revision or null if the old revision does not exist or does not belong to a page. |
133 | * @var Title|null |
134 | */ |
135 | protected $mOldPage; |
136 | |
137 | /** |
138 | * Title of new revision or null if the new revision does not exist or does not belong to a page. |
139 | * @var Title|null |
140 | */ |
141 | protected $mNewPage; |
142 | |
143 | /** |
144 | * Change tags of old revision or null if it does not exist / is not saved. |
145 | * @var string|false|null |
146 | */ |
147 | private $mOldTags; |
148 | |
149 | /** |
150 | * Change tags of new revision or null if it does not exist / is not saved. |
151 | * @var string|null |
152 | */ |
153 | private $mNewTags; |
154 | |
155 | /** |
156 | * @var Content|null |
157 | * @deprecated since 1.32, content slots are now handled by the corresponding SlotDiffRenderer. |
158 | * This property is set to the content of the main slot, but not actually used for the main diff. |
159 | */ |
160 | private $mOldContent; |
161 | |
162 | /** |
163 | * @var Content|null |
164 | * @deprecated since 1.32, content slots are now handled by the corresponding SlotDiffRenderer. |
165 | * This property is set to the content of the main slot, but not actually used for the main diff. |
166 | */ |
167 | private $mNewContent; |
168 | |
169 | /** @var Language */ |
170 | protected $mDiffLang; |
171 | |
172 | /** @var bool Have the revisions IDs been loaded */ |
173 | private $mRevisionsIdsLoaded = false; |
174 | |
175 | /** @var bool Have the revisions been loaded */ |
176 | protected $mRevisionsLoaded = false; |
177 | |
178 | /** @var int How many text blobs have been loaded, 0, 1 or 2? */ |
179 | protected $mTextLoaded = 0; |
180 | |
181 | /** |
182 | * Was the content overridden via setContent()? |
183 | * If the content was overridden, most internal state (e.g. mOldid or mOldRev) should be ignored |
184 | * and only mOldContent and mNewContent is reliable. |
185 | * (Note that setRevisions() does not set this flag as in that case all properties are |
186 | * overridden and remain consistent with each other, so no special handling is needed.) |
187 | * @var bool |
188 | */ |
189 | protected $isContentOverridden = false; |
190 | |
191 | /** @var bool Was the diff fetched from cache? */ |
192 | protected $mCacheHit = false; |
193 | |
194 | /** @var string|null Cache key if the diff was fetched from cache */ |
195 | private $cacheHitKey = null; |
196 | |
197 | /** |
198 | * Set this to true to add debug info to the HTML output. |
199 | * Warning: this may cause RSS readers to spuriously mark articles as "new" |
200 | * (T22601) |
201 | * @var bool |
202 | */ |
203 | public $enableDebugComment = false; |
204 | |
205 | /** @var bool If true, line X is not displayed when X is 1, for example |
206 | * to increase readability and conserve space with many small diffs. |
207 | */ |
208 | protected $mReducedLineNumbers = false; |
209 | |
210 | /** @var string Link to action=markpatrolled */ |
211 | protected $mMarkPatrolledLink = null; |
212 | |
213 | /** @var bool Show rev_deleted content if allowed */ |
214 | protected $unhide = false; |
215 | |
216 | /** @var bool Refresh the diff cache */ |
217 | protected $mRefreshCache = false; |
218 | |
219 | /** @var SlotDiffRenderer[]|null DifferenceEngine classes for the slots, keyed by role name. */ |
220 | protected $slotDiffRenderers = null; |
221 | |
222 | /** |
223 | * Temporary hack for B/C while slot diff related methods of DifferenceEngine are being |
224 | * deprecated. When true, we are inside a DifferenceEngineSlotDiffRenderer and |
225 | * $slotDiffRenderers should not be used. |
226 | * @var bool |
227 | */ |
228 | protected $isSlotDiffRenderer = false; |
229 | |
230 | /** |
231 | * A set of options that will be passed to the SlotDiffRenderer upon creation |
232 | * @var array |
233 | */ |
234 | private $slotDiffOptions = []; |
235 | |
236 | /** |
237 | * Extra query parameters to be appended to diff page links |
238 | * @var array |
239 | */ |
240 | private $extraQueryParams = []; |
241 | |
242 | /** @var ManifoldTextDiffer|null */ |
243 | private $textDiffer; |
244 | |
245 | protected LinkRenderer $linkRenderer; |
246 | private IContentHandlerFactory $contentHandlerFactory; |
247 | private RevisionStore $revisionStore; |
248 | private ArchivedRevisionLookup $archivedRevisionLookup; |
249 | private HookRunner $hookRunner; |
250 | private WikiPageFactory $wikiPageFactory; |
251 | private UserOptionsLookup $userOptionsLookup; |
252 | private CommentFormatter $commentFormatter; |
253 | private IConnectionProvider $dbProvider; |
254 | private UserGroupManager $userGroupManager; |
255 | private UserEditTracker $userEditTracker; |
256 | private UserIdentityUtils $userIdentityUtils; |
257 | |
258 | /** @var Message[] */ |
259 | private $revisionLoadErrors = []; |
260 | |
261 | /** |
262 | * @param IContextSource|null $context Context to use, anything else will be ignored |
263 | * @param int $old Old ID we want to show and diff with. |
264 | * @param string|int $new Either revision ID or 'prev' or 'next'. Default: 0. |
265 | * @param int $rcid Deprecated, no longer used! |
266 | * @param bool $refreshCache If set, refreshes the diff cache |
267 | * @param bool $unhide If set, allow viewing deleted revs |
268 | */ |
269 | public function __construct( $context = null, $old = 0, $new = 0, $rcid = 0, |
270 | $refreshCache = false, $unhide = false |
271 | ) { |
272 | if ( $context instanceof IContextSource ) { |
273 | $this->setContext( $context ); |
274 | } |
275 | |
276 | wfDebug( "DifferenceEngine old '$old' new '$new' rcid '$rcid'" ); |
277 | |
278 | $this->mOldid = $old; |
279 | $this->mNewid = $new; |
280 | $this->mRefreshCache = $refreshCache; |
281 | $this->unhide = $unhide; |
282 | |
283 | $services = MediaWikiServices::getInstance(); |
284 | $this->linkRenderer = $services->getLinkRenderer(); |
285 | $this->contentHandlerFactory = $services->getContentHandlerFactory(); |
286 | $this->revisionStore = $services->getRevisionStore(); |
287 | $this->archivedRevisionLookup = $services->getArchivedRevisionLookup(); |
288 | $this->hookRunner = new HookRunner( $services->getHookContainer() ); |
289 | $this->wikiPageFactory = $services->getWikiPageFactory(); |
290 | $this->userOptionsLookup = $services->getUserOptionsLookup(); |
291 | $this->commentFormatter = $services->getCommentFormatter(); |
292 | $this->dbProvider = $services->getConnectionProvider(); |
293 | $this->userGroupManager = $services->getUserGroupManager(); |
294 | $this->userEditTracker = $services->getUserEditTracker(); |
295 | $this->userIdentityUtils = $services->getUserIdentityUtils(); |
296 | } |
297 | |
298 | /** |
299 | * @return SlotDiffRenderer[] Diff renderers for each slot, keyed by role name. |
300 | * Includes slots only present in one of the revisions. Does not include slots |
301 | * for which content is identical in the two revisions. |
302 | */ |
303 | protected function getSlotDiffRenderers() { |
304 | if ( $this->isSlotDiffRenderer ) { |
305 | throw new LogicException( __METHOD__ . ' called in slot diff renderer mode' ); |
306 | } |
307 | |
308 | if ( $this->slotDiffRenderers === null ) { |
309 | if ( !$this->loadRevisionData() ) { |
310 | return []; |
311 | } |
312 | |
313 | $slotContents = $this->getSlotContents(); |
314 | $this->slotDiffRenderers = []; |
315 | foreach ( $slotContents as $role => $contents ) { |
316 | if ( $contents['new'] && $contents['old'] |
317 | && $contents['new']->equals( $contents['old'] ) |
318 | ) { |
319 | // Do not produce a diff of identical content |
320 | continue; |
321 | } |
322 | $handler = ( $contents['new'] ?: $contents['old'] )->getContentHandler(); |
323 | $this->slotDiffRenderers[$role] = $handler->getSlotDiffRenderer( |
324 | $this->getContext(), |
325 | $this->slotDiffOptions + [ |
326 | 'contentLanguage' => $this->getDiffLang()->getCode(), |
327 | 'textDiffer' => $this->getTextDiffer() |
328 | ] |
329 | ); |
330 | } |
331 | } |
332 | |
333 | return $this->slotDiffRenderers; |
334 | } |
335 | |
336 | /** |
337 | * Mark this DifferenceEngine as a slot renderer (as opposed to a page renderer). |
338 | * This is used in legacy mode when the DifferenceEngine is wrapped in a |
339 | * DifferenceEngineSlotDiffRenderer. |
340 | * @internal For use by DifferenceEngineSlotDiffRenderer only. |
341 | */ |
342 | public function markAsSlotDiffRenderer() { |
343 | $this->isSlotDiffRenderer = true; |
344 | } |
345 | |
346 | /** |
347 | * Get the old and new content objects for all slots. |
348 | * This method does not do any permission checks. |
349 | * @return (Content|null)[][] [ role => [ 'old' => Content|null, 'new' => Content|null ], ... ] |
350 | */ |
351 | protected function getSlotContents() { |
352 | if ( $this->isContentOverridden ) { |
353 | return [ |
354 | SlotRecord::MAIN => [ 'old' => $this->mOldContent, 'new' => $this->mNewContent ] |
355 | ]; |
356 | } elseif ( !$this->loadRevisionData() ) { |
357 | return []; |
358 | } |
359 | |
360 | $newSlots = $this->mNewRevisionRecord->getPrimarySlots()->getSlots(); |
361 | $oldSlots = $this->mOldRevisionRecord ? |
362 | $this->mOldRevisionRecord->getPrimarySlots()->getSlots() : |
363 | []; |
364 | // The order here will determine the visual order of the diff. The current logic is |
365 | // slots of the new revision first in natural order, then deleted ones. This is ad hoc |
366 | // and should not be relied on - in the future we may want the ordering to depend |
367 | // on the page type. |
368 | $roles = array_keys( array_merge( $newSlots, $oldSlots ) ); |
369 | |
370 | $slots = []; |
371 | foreach ( $roles as $role ) { |
372 | $slots[$role] = [ |
373 | 'old' => $this->loadSingleSlot( |
374 | $oldSlots[$role] ?? null, |
375 | 'old' |
376 | ), |
377 | 'new' => $this->loadSingleSlot( |
378 | $newSlots[$role] ?? null, |
379 | 'new' |
380 | ) |
381 | ]; |
382 | } |
383 | // move main slot to front |
384 | if ( isset( $slots[SlotRecord::MAIN] ) ) { |
385 | $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots; |
386 | } |
387 | return $slots; |
388 | } |
389 | |
390 | /** |
391 | * Load the content of a single slot record |
392 | * |
393 | * @param SlotRecord|null $slot |
394 | * @param string $which "new" or "old" |
395 | * @return Content|null |
396 | */ |
397 | private function loadSingleSlot( ?SlotRecord $slot, string $which ) { |
398 | if ( !$slot ) { |
399 | return null; |
400 | } |
401 | try { |
402 | return $slot->getContent(); |
403 | } catch ( BadRevisionException $e ) { |
404 | $this->addRevisionLoadError( $which ); |
405 | return null; |
406 | } |
407 | } |
408 | |
409 | /** |
410 | * Set a message to show as a notice at the top of the page |
411 | * |
412 | * @param string $which "new" or "old" |
413 | */ |
414 | private function addRevisionLoadError( $which ) { |
415 | $this->revisionLoadErrors[] = $this->msg( $which === 'new' |
416 | ? 'difference-bad-new-revision' : 'difference-bad-old-revision' |
417 | ); |
418 | } |
419 | |
420 | /** |
421 | * If errors were encountered while loading the revision contents, this |
422 | * will return an array of Messages describing the errors. |
423 | * |
424 | * @return Message[] |
425 | */ |
426 | public function getRevisionLoadErrors() { |
427 | return $this->revisionLoadErrors; |
428 | } |
429 | |
430 | /** |
431 | * Determine whether there was an error loading the new revision |
432 | * @return bool |
433 | */ |
434 | private function hasNewRevisionLoadError() { |
435 | foreach ( $this->revisionLoadErrors as $error ) { |
436 | if ( $error->getKey() === 'difference-bad-new-revision' ) { |
437 | return true; |
438 | } |
439 | } |
440 | return false; |
441 | } |
442 | |
443 | /** @inheritDoc */ |
444 | public function getTitle() { |
445 | // T202454 avoid errors when there is no title |
446 | return parent::getTitle() ?: Title::makeTitle( NS_SPECIAL, 'BadTitle/DifferenceEngine' ); |
447 | } |
448 | |
449 | /** |
450 | * Set reduced line numbers mode. |
451 | * When set, line X is not displayed when X is 1, for example to increase readability and |
452 | * conserve space with many small diffs. |
453 | * @param bool $value |
454 | */ |
455 | public function setReducedLineNumbers( $value = true ) { |
456 | $this->mReducedLineNumbers = $value; |
457 | } |
458 | |
459 | /** |
460 | * Get the language in which the diff text is written |
461 | * |
462 | * @return Language |
463 | */ |
464 | public function getDiffLang() { |
465 | # Default language in which the diff text is written. |
466 | $this->mDiffLang ??= $this->getDefaultLanguage(); |
467 | return $this->mDiffLang; |
468 | } |
469 | |
470 | /** |
471 | * Get the language to use if none has been set by setTextLanguage(). |
472 | * Wikibase overrides this to use the user language. |
473 | * |
474 | * @return Language |
475 | */ |
476 | protected function getDefaultLanguage() { |
477 | return $this->getTitle()->getPageLanguage(); |
478 | } |
479 | |
480 | /** |
481 | * @return bool |
482 | */ |
483 | public function wasCacheHit() { |
484 | return $this->mCacheHit; |
485 | } |
486 | |
487 | /** |
488 | * Get the ID of old revision (left pane) of the diff. 0 for the revision |
489 | * previous to getNewid(), false if the old revision does not exist, null |
490 | * if it's unsaved. |
491 | * To get a real revision ID instead of 0, call loadRevisionData() first. |
492 | * @return int|false|null |
493 | */ |
494 | public function getOldid() { |
495 | $this->loadRevisionIds(); |
496 | |
497 | return $this->mOldid; |
498 | } |
499 | |
500 | /** |
501 | * Get the ID of new revision (right pane) of the diff. 0 for the current revision, |
502 | * false if the new revision does not exist, null if it's unsaved. |
503 | * To get a real revision ID instead of 0, call loadRevisionData() first. |
504 | * @return int|false|null |
505 | */ |
506 | public function getNewid() { |
507 | $this->loadRevisionIds(); |
508 | |
509 | return $this->mNewid; |
510 | } |
511 | |
512 | /** |
513 | * Get the left side of the diff. |
514 | * Could be null when the first revision of the page is diffed to 'prev' (or in the case of |
515 | * load failure). |
516 | * @return RevisionRecord|null |
517 | */ |
518 | public function getOldRevision() { |
519 | return $this->mOldRevisionRecord ?: null; |
520 | } |
521 | |
522 | /** |
523 | * Get the right side of the diff. |
524 | * Should not be null but can still happen in the case of load failure. |
525 | * @return RevisionRecord|null |
526 | */ |
527 | public function getNewRevision() { |
528 | return $this->mNewRevisionRecord; |
529 | } |
530 | |
531 | /** |
532 | * Look up a special:Undelete link to the given deleted revision id, |
533 | * as a workaround for being unable to load deleted diffs in currently. |
534 | * |
535 | * @param int $id Revision ID |
536 | * |
537 | * @return string|bool Link HTML or false |
538 | */ |
539 | public function deletedLink( $id ) { |
540 | if ( $this->getAuthority()->isAllowed( 'deletedhistory' ) ) { |
541 | $revRecord = $this->archivedRevisionLookup->getArchivedRevisionRecord( null, $id ); |
542 | if ( $revRecord ) { |
543 | $title = Title::newFromPageIdentity( $revRecord->getPage() ); |
544 | |
545 | return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( [ |
546 | 'target' => $title->getPrefixedText(), |
547 | 'timestamp' => $revRecord->getTimestamp() |
548 | ] ); |
549 | } |
550 | } |
551 | |
552 | return false; |
553 | } |
554 | |
555 | /** |
556 | * Build a wikitext link toward a deleted revision, if viewable. |
557 | * |
558 | * @param int $id Revision ID |
559 | * |
560 | * @return string Wikitext fragment |
561 | */ |
562 | public function deletedIdMarker( $id ) { |
563 | $link = $this->deletedLink( $id ); |
564 | if ( $link ) { |
565 | return "[$link $id]"; |
566 | } else { |
567 | return (string)$id; |
568 | } |
569 | } |
570 | |
571 | private function showMissingRevision() { |
572 | $out = $this->getOutput(); |
573 | |
574 | $missing = []; |
575 | if ( $this->mOldid && ( !$this->mOldRevisionRecord || !$this->mOldContent ) ) { |
576 | $missing[] = $this->deletedIdMarker( $this->mOldid ); |
577 | } |
578 | if ( !$this->mNewRevisionRecord || !$this->mNewContent ) { |
579 | $missing[] = $this->deletedIdMarker( $this->mNewid ); |
580 | } |
581 | |
582 | $out->setPageTitleMsg( $this->msg( 'errorpagetitle' ) ); |
583 | $msg = $this->msg( 'difference-missing-revision' ) |
584 | ->params( $this->getLanguage()->listToText( $missing ) ) |
585 | ->numParams( count( $missing ) ) |
586 | ->parseAsBlock(); |
587 | $out->addHTML( $msg ); |
588 | } |
589 | |
590 | /** |
591 | * Checks whether one of the given Revisions was deleted |
592 | * |
593 | * @return bool |
594 | */ |
595 | public function hasDeletedRevision() { |
596 | $this->loadRevisionData(); |
597 | return ( |
598 | $this->mNewRevisionRecord && |
599 | $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT ) |
600 | ) || |
601 | ( |
602 | $this->mOldRevisionRecord && |
603 | $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT ) |
604 | ); |
605 | } |
606 | |
607 | /** |
608 | * Get the permission errors associated with the revisions for the current diff. |
609 | * |
610 | * @param Authority $performer |
611 | * @return array[] Array of arrays of the arguments to wfMessage to explain permissions problems. |
612 | */ |
613 | public function getPermissionErrors( Authority $performer ) { |
614 | $this->loadRevisionData(); |
615 | $permStatus = PermissionStatus::newEmpty(); |
616 | if ( $this->mNewPage ) { |
617 | $performer->authorizeRead( 'read', $this->mNewPage, $permStatus ); |
618 | } |
619 | if ( $this->mOldPage ) { |
620 | $performer->authorizeRead( 'read', $this->mOldPage, $permStatus ); |
621 | } |
622 | return $permStatus->toLegacyErrorArray(); |
623 | } |
624 | |
625 | /** |
626 | * Checks whether one of the given Revisions was suppressed |
627 | * |
628 | * @return bool |
629 | */ |
630 | public function hasSuppressedRevision() { |
631 | return $this->hasDeletedRevision() && ( |
632 | ( $this->mOldRevisionRecord && |
633 | $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) || |
634 | ( $this->mNewRevisionRecord && |
635 | $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) |
636 | ); |
637 | } |
638 | |
639 | /** |
640 | * Renders user associated edit count |
641 | * |
642 | * @param UserIdentity $user |
643 | * @return string |
644 | */ |
645 | private function getUserEditCount( $user ): string { |
646 | $editCount = $this->userEditTracker->getUserEditCount( $user ); |
647 | if ( $editCount === null ) { |
648 | return ''; |
649 | } |
650 | |
651 | return Html::rawElement( 'div', [ |
652 | 'class' => 'mw-diff-usereditcount', |
653 | ], |
654 | $this->msg( |
655 | 'diff-user-edits', |
656 | $this->getLanguage()->formatNum( $editCount ) |
657 | )->parse() |
658 | ); |
659 | } |
660 | |
661 | /** |
662 | * Renders user roles |
663 | * |
664 | * @param UserIdentity $user |
665 | * @return string |
666 | */ |
667 | private function getUserRoles( UserIdentity $user ) { |
668 | if ( !$this->userIdentityUtils->isNamed( $user ) ) { |
669 | return ''; |
670 | } |
671 | $userGroups = $this->userGroupManager->getUserGroups( $user ); |
672 | $userGroupLinks = []; |
673 | foreach ( $userGroups as $group ) { |
674 | $userGroupLinks[] = UserGroupMembership::getLinkHTML( $group, $this->getContext() ); |
675 | } |
676 | return Html::rawElement( 'div', [ |
677 | 'class' => 'mw-diff-userroles', |
678 | ], $this->getLanguage()->commaList( $userGroupLinks ) ); |
679 | } |
680 | |
681 | /** |
682 | * Renders user associated meta data |
683 | * |
684 | * @param UserIdentity|null $user |
685 | * @return string |
686 | */ |
687 | private function getUserMetaData( ?UserIdentity $user ) { |
688 | if ( !$user ) { |
689 | return ''; |
690 | } |
691 | return Html::rawElement( 'div', [ |
692 | 'class' => 'mw-diff-usermetadata', |
693 | ], $this->getUserRoles( $user ) . $this->getUserEditCount( $user ) ); |
694 | } |
695 | |
696 | /** |
697 | * Checks whether the current user has permission for accessing the revisions of the diff. |
698 | * Note that this does not check whether the user has permission to view the page, it only |
699 | * checks revdelete permissions. |
700 | * |
701 | * It is the caller's responsibility to call |
702 | * $this->getUserPermissionErrors or similar checks. |
703 | * |
704 | * @param Authority $performer |
705 | * @return bool |
706 | */ |
707 | public function isUserAllowedToSeeRevisions( Authority $performer ) { |
708 | $this->loadRevisionData(); |
709 | |
710 | if ( $this->mOldRevisionRecord && !$this->mOldRevisionRecord->userCan( |
711 | RevisionRecord::DELETED_TEXT, |
712 | $performer |
713 | ) ) { |
714 | return false; |
715 | } |
716 | |
717 | // $this->mNewRev will only be falsy if a loading error occurred |
718 | // (in which case the user is allowed to see). |
719 | return !$this->mNewRevisionRecord || $this->mNewRevisionRecord->userCan( |
720 | RevisionRecord::DELETED_TEXT, |
721 | $performer |
722 | ); |
723 | } |
724 | |
725 | /** |
726 | * Checks whether the diff should be hidden from the current user |
727 | * This is based on whether the user is allowed to see it and has specifically asked to see it. |
728 | * |
729 | * @param Authority $performer |
730 | * @return bool |
731 | */ |
732 | public function shouldBeHiddenFromUser( Authority $performer ) { |
733 | return $this->hasDeletedRevision() && ( !$this->unhide || |
734 | !$this->isUserAllowedToSeeRevisions( $performer ) ); |
735 | } |
736 | |
737 | /** |
738 | * @param bool $diffOnly |
739 | */ |
740 | public function showDiffPage( $diffOnly = false ) { |
741 | # Allow frames except in certain special cases |
742 | $out = $this->getOutput(); |
743 | $out->getMetadata()->setPreventClickjacking( false ); |
744 | $out->setRobotPolicy( 'noindex,nofollow' ); |
745 | |
746 | // Allow extensions to add any extra output here |
747 | $this->hookRunner->onDifferenceEngineShowDiffPage( $out ); |
748 | |
749 | if ( !$this->loadRevisionData() ) { |
750 | if ( $this->hookRunner->onDifferenceEngineShowDiffPageMaybeShowMissingRevision( $this ) ) { |
751 | $this->showMissingRevision(); |
752 | } |
753 | return; |
754 | } |
755 | |
756 | $user = $this->getUser(); |
757 | $permErrors = $this->getPermissionErrors( $this->getAuthority() ); |
758 | if ( $permErrors ) { |
759 | throw new PermissionsError( 'read', $permErrors ); |
760 | } |
761 | |
762 | $rollback = ''; |
763 | |
764 | $query = $this->extraQueryParams; |
765 | # Carry over 'diffonly' param via navigation links |
766 | if ( $diffOnly != MediaWikiServices::getInstance() |
767 | ->getUserOptionsLookup()->getBoolOption( $user, 'diffonly' ) |
768 | ) { |
769 | $query['diffonly'] = $diffOnly; |
770 | } |
771 | # Cascade unhide param in links for easy deletion browsing |
772 | if ( $this->unhide ) { |
773 | $query['unhide'] = 1; |
774 | } |
775 | |
776 | # Check if one of the revisions is deleted/suppressed |
777 | $deleted = $this->hasDeletedRevision(); |
778 | $suppressed = $this->hasSuppressedRevision(); |
779 | $allowed = $this->isUserAllowedToSeeRevisions( $this->getAuthority() ); |
780 | |
781 | $revisionTools = []; |
782 | $breadCrumbs = ''; |
783 | |
784 | # mOldRevisionRecord is false if the difference engine is called with a "vague" query for |
785 | # a diff between a version V and its previous version V' AND the version V |
786 | # is the first version of that article. In that case, V' does not exist. |
787 | if ( $this->mOldRevisionRecord === false ) { |
788 | if ( $this->mNewPage ) { |
789 | $out->setPageTitleMsg( |
790 | $this->msg( 'difference-title' )->plaintextParams( $this->mNewPage->getPrefixedText() ) |
791 | ); |
792 | } |
793 | $samePage = true; |
794 | $oldHeader = ''; |
795 | // Allow extensions to change the $oldHeader variable |
796 | $this->hookRunner->onDifferenceEngineOldHeaderNoOldRev( $oldHeader ); |
797 | } else { |
798 | $this->hookRunner->onDifferenceEngineViewHeader( $this ); |
799 | |
800 | if ( !$this->mOldPage || !$this->mNewPage ) { |
801 | // XXX say something to the user? |
802 | $samePage = false; |
803 | } elseif ( $this->mNewPage->equals( $this->mOldPage ) ) { |
804 | $out->setPageTitleMsg( |
805 | $this->msg( 'difference-title' )->plaintextParams( $this->mNewPage->getPrefixedText() ) |
806 | ); |
807 | $samePage = true; |
808 | } else { |
809 | $out->setPageTitleMsg( $this->msg( 'difference-title-multipage' )->plaintextParams( |
810 | $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) ); |
811 | $out->addSubtitle( $this->msg( 'difference-multipage' ) ); |
812 | $samePage = false; |
813 | } |
814 | |
815 | if ( $samePage && $this->mNewPage && |
816 | $this->getAuthority()->probablyCan( 'edit', $this->mNewPage ) |
817 | ) { |
818 | if ( $this->mNewRevisionRecord->isCurrent() && |
819 | $this->getAuthority()->probablyCan( 'rollback', $this->mNewPage ) |
820 | ) { |
821 | $rollbackLink = Linker::generateRollback( |
822 | $this->mNewRevisionRecord, |
823 | $this->getContext(), |
824 | [ 'noBrackets' ] |
825 | ); |
826 | if ( $rollbackLink ) { |
827 | $out->getMetadata()->setPreventClickjacking( true ); |
828 | $rollback = "\u{00A0}\u{00A0}\u{00A0}" . $rollbackLink; |
829 | } |
830 | } |
831 | |
832 | if ( $this->userCanEdit( $this->mOldRevisionRecord ) && |
833 | $this->userCanEdit( $this->mNewRevisionRecord ) |
834 | ) { |
835 | $undoLink = $this->linkRenderer->makeKnownLink( |
836 | $this->mNewPage, |
837 | $this->msg( 'editundo' )->text(), |
838 | [ 'title' => Linker::titleAttrib( 'undo' ) ], |
839 | [ |
840 | 'action' => 'edit', |
841 | 'undoafter' => $this->mOldid, |
842 | 'undo' => $this->mNewid |
843 | ] |
844 | ); |
845 | $revisionTools['mw-diff-undo'] = $undoLink; |
846 | } |
847 | } |
848 | # Make "previous revision link" |
849 | $hasPrevious = $samePage && $this->mOldPage && |
850 | $this->revisionStore->getPreviousRevision( $this->mOldRevisionRecord ); |
851 | if ( $hasPrevious ) { |
852 | $prevlinkQuery = [ 'diff' => 'prev', 'oldid' => $this->mOldid ] + $query; |
853 | $prevlink = $this->linkRenderer->makeKnownLink( |
854 | $this->mOldPage, |
855 | $this->msg( 'previousdiff' )->text(), |
856 | [ 'id' => 'differences-prevlink' ], |
857 | $prevlinkQuery |
858 | ); |
859 | $breadCrumbs .= $this->linkRenderer->makeKnownLink( |
860 | $this->mOldPage, |
861 | $this->msg( 'previousdiff' )->text(), |
862 | [ |
863 | 'class' => 'mw-diff-revision-history-link-previous' |
864 | ], |
865 | $prevlinkQuery |
866 | ); |
867 | } else { |
868 | $prevlink = "\u{00A0}"; |
869 | } |
870 | |
871 | if ( $this->mOldRevisionRecord->isMinor() ) { |
872 | $oldminor = ChangesList::flag( 'minor' ); |
873 | } else { |
874 | $oldminor = ''; |
875 | } |
876 | |
877 | $oldRevRecord = $this->mOldRevisionRecord; |
878 | |
879 | $ldel = $this->revisionDeleteLink( $oldRevRecord ); |
880 | $oldRevisionHeader = $this->getRevisionHeader( $oldRevRecord, 'complete' ); |
881 | $oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff', $this->getContext() ); |
882 | $oldRevComment = $this->commentFormatter |
883 | ->formatRevision( |
884 | $oldRevRecord, $user, !$diffOnly, !$this->unhide, /** disable parentheseses */ false |
885 | ); |
886 | |
887 | if ( $oldRevComment === '' ) { |
888 | $defaultComment = $this->msg( 'changeslist-nocomment' )->escaped(); |
889 | $oldRevComment = "<span class=\"comment mw-comment-none\">$defaultComment</span>"; |
890 | } |
891 | |
892 | $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' . |
893 | '<div id="mw-diff-otitle2">' . |
894 | Linker::revUserTools( $oldRevRecord, !$this->unhide ) . |
895 | $this->getUserMetaData( $oldRevRecord->getUser() ) . |
896 | '</div>' . |
897 | '<div id="mw-diff-otitle3">' . $oldminor . $oldRevComment . $ldel . '</div>' . |
898 | '<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' . |
899 | '<div id="mw-diff-otitle4">' . $prevlink . '</div>'; |
900 | |
901 | // Allow extensions to change the $oldHeader variable |
902 | $this->hookRunner->onDifferenceEngineOldHeader( |
903 | $this, $oldHeader, $prevlink, $oldminor, $diffOnly, $ldel, $this->unhide ); |
904 | } |
905 | |
906 | $out->addJsConfigVars( [ |
907 | 'wgDiffOldId' => $this->mOldid, |
908 | 'wgDiffNewId' => $this->mNewid, |
909 | ] ); |
910 | |
911 | # Make "next revision link" |
912 | # Skip next link on the top revision |
913 | if ( $samePage && $this->mNewPage && !$this->mNewRevisionRecord->isCurrent() ) { |
914 | $nextlinkQuery = [ 'diff' => 'next', 'oldid' => $this->mNewid ] + $query; |
915 | $nextlink = $this->linkRenderer->makeKnownLink( |
916 | $this->mNewPage, |
917 | $this->msg( 'nextdiff' )->text(), |
918 | [ 'id' => 'differences-nextlink' ], |
919 | $nextlinkQuery |
920 | ); |
921 | $breadCrumbs .= $this->linkRenderer->makeKnownLink( |
922 | $this->mNewPage, |
923 | $this->msg( 'nextdiff' )->text(), |
924 | [ |
925 | 'class' => 'mw-diff-revision-history-link-next' |
926 | ], |
927 | $nextlinkQuery |
928 | ); |
929 | } else { |
930 | $nextlink = "\u{00A0}"; |
931 | } |
932 | |
933 | if ( $this->mNewRevisionRecord->isMinor() ) { |
934 | $newminor = ChangesList::flag( 'minor' ); |
935 | } else { |
936 | $newminor = ''; |
937 | } |
938 | |
939 | # Handle RevisionDelete links... |
940 | $rdel = $this->revisionDeleteLink( $this->mNewRevisionRecord ); |
941 | |
942 | # Allow extensions to define their own revision tools |
943 | $this->hookRunner->onDiffTools( |
944 | $this->mNewRevisionRecord, |
945 | $revisionTools, |
946 | $this->mOldRevisionRecord ?: null, |
947 | $user |
948 | ); |
949 | |
950 | $formattedRevisionTools = []; |
951 | // Put each one in parentheses (poor man's button) |
952 | foreach ( $revisionTools as $key => $tool ) { |
953 | $toolClass = is_string( $key ) ? $key : 'mw-diff-tool'; |
954 | $element = Html::rawElement( |
955 | 'span', |
956 | [ 'class' => $toolClass ], |
957 | $tool |
958 | ); |
959 | $formattedRevisionTools[] = $element; |
960 | } |
961 | |
962 | $newRevRecord = $this->mNewRevisionRecord; |
963 | |
964 | $newRevisionHeader = $this->getRevisionHeader( $newRevRecord, 'complete' ) . |
965 | ' ' . implode( ' ', $formattedRevisionTools ); |
966 | $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff', $this->getContext() ); |
967 | $newRevComment = $this->commentFormatter->formatRevision( |
968 | $newRevRecord, $user, !$diffOnly, !$this->unhide, /** disable parentheseses */ false |
969 | ); |
970 | |
971 | if ( $newRevComment === '' ) { |
972 | $defaultComment = $this->msg( 'changeslist-nocomment' )->escaped(); |
973 | $newRevComment = "<span class=\"comment mw-comment-none\">$defaultComment</span>"; |
974 | } |
975 | |
976 | $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' . |
977 | '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $newRevRecord, !$this->unhide ) . |
978 | $rollback . |
979 | $this->getUserMetaData( $newRevRecord->getUser() ) . |
980 | '</div>' . |
981 | '<div id="mw-diff-ntitle3">' . $newminor . $newRevComment . $rdel . '</div>' . |
982 | '<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' . |
983 | '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>'; |
984 | |
985 | // Allow extensions to change the $newHeader variable |
986 | $this->hookRunner->onDifferenceEngineNewHeader( $this, $newHeader, |
987 | $formattedRevisionTools, $nextlink, $rollback, $newminor, $diffOnly, |
988 | $rdel, $this->unhide ); |
989 | |
990 | $out->addHTML( |
991 | Html::rawElement( 'div', [ |
992 | 'class' => 'mw-diff-revision-history-links' |
993 | ], $breadCrumbs ) |
994 | ); |
995 | $addMessageBoxStyles = false; |
996 | # If the diff cannot be shown due to a deleted revision, then output |
997 | # the diff header and links to unhide (if available)... |
998 | if ( $this->shouldBeHiddenFromUser( $this->getAuthority() ) ) { |
999 | $this->showDiffStyle(); |
1000 | $multi = $this->getMultiNotice(); |
1001 | $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) ); |
1002 | if ( !$allowed ) { |
1003 | # Give explanation for why revision is not visible |
1004 | $msg = [ $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff' ]; |
1005 | } else { |
1006 | # Give explanation and add a link to view the diff... |
1007 | $query = $this->getRequest()->appendQueryValue( 'unhide', '1' ); |
1008 | $msg = [ |
1009 | $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff', |
1010 | $this->getTitle()->getFullURL( $query ) |
1011 | ]; |
1012 | } |
1013 | $out->addHTML( Html::warningBox( $this->msg( ...$msg )->parse(), 'plainlinks' ) ); |
1014 | $addMessageBoxStyles = true; |
1015 | # Otherwise, output a regular diff... |
1016 | } else { |
1017 | # Add deletion notice if the user is viewing deleted content |
1018 | $notice = ''; |
1019 | if ( $deleted ) { |
1020 | $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view'; |
1021 | $notice = Html::warningBox( $this->msg( $msg )->parse(), 'plainlinks' ); |
1022 | $addMessageBoxStyles = true; |
1023 | } |
1024 | |
1025 | # Add an error if the content can't be loaded |
1026 | $this->getSlotContents(); |
1027 | foreach ( $this->getRevisionLoadErrors() as $msg ) { |
1028 | $notice .= Html::warningBox( $msg->parse() ); |
1029 | $addMessageBoxStyles = true; |
1030 | } |
1031 | |
1032 | // Check if inline switcher will be needed |
1033 | if ( $this->getTextDiffer()->hasFormat( 'inline' ) ) { |
1034 | $out->enableOOUI(); |
1035 | } |
1036 | |
1037 | $this->showTablePrefixes(); |
1038 | $this->showDiff( $oldHeader, $newHeader, $notice ); |
1039 | if ( !$diffOnly ) { |
1040 | $this->renderNewRevision(); |
1041 | } |
1042 | |
1043 | // Allow extensions to optionally not show the final patrolled link |
1044 | if ( $this->hookRunner->onDifferenceEngineRenderRevisionShowFinalPatrolLink() ) { |
1045 | # Add redundant patrol link on bottom... |
1046 | $out->addHTML( $this->markPatrolledLink() ); |
1047 | } |
1048 | } |
1049 | if ( $addMessageBoxStyles ) { |
1050 | $out->addModuleStyles( 'mediawiki.codex.messagebox.styles' ); |
1051 | } |
1052 | } |
1053 | |
1054 | /** |
1055 | * Add table prefixes |
1056 | */ |
1057 | private function showTablePrefixes() { |