Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
84.51% |
371 / 439 |
|
41.67% |
5 / 12 |
CRAP | |
0.00% |
0 / 1 |
EnhancedChangesList | |
84.51% |
371 / 439 |
|
41.67% |
5 / 12 |
145.97 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
beginRecentChangesList | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
recentChangesLine | |
78.57% |
11 / 14 |
|
0.00% |
0 / 1 |
3.09 | |||
addCacheEntry | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
makeCacheGroupingKey | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
recentChangesBlockGroup | |
86.99% |
107 / 123 |
|
0.00% |
0 / 1 |
35.40 | |||
getLineData | |
89.74% |
70 / 78 |
|
0.00% |
0 / 1 |
16.28 | |||
getLogText | |
60.00% |
48 / 80 |
|
0.00% |
0 / 1 |
45.60 | |||
recentChangesBlockLine | |
98.80% |
82 / 83 |
|
0.00% |
0 / 1 |
16 | |||
getDiffHistLinks | |
77.78% |
21 / 27 |
|
0.00% |
0 / 1 |
6.40 | |||
recentChangesBlock | |
80.00% |
8 / 10 |
|
0.00% |
0 / 1 |
5.20 | |||
endRecentChangesList | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | use MediaWiki\Context\IContextSource; |
4 | use MediaWiki\Html\Html; |
5 | use MediaWiki\Html\TemplateParser; |
6 | use MediaWiki\MainConfigNames; |
7 | use MediaWiki\Parser\Sanitizer; |
8 | use MediaWiki\Revision\RevisionRecord; |
9 | use MediaWiki\SpecialPage\SpecialPage; |
10 | use MediaWiki\Title\Title; |
11 | |
12 | /** |
13 | * Generates a list of changes using an Enhanced system (uses javascript). |
14 | * |
15 | * This program is free software; you can redistribute it and/or modify |
16 | * it under the terms of the GNU General Public License as published by |
17 | * the Free Software Foundation; either version 2 of the License, or |
18 | * (at your option) any later version. |
19 | * |
20 | * This program is distributed in the hope that it will be useful, |
21 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
22 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
23 | * GNU General Public License for more details. |
24 | * |
25 | * You should have received a copy of the GNU General Public License along |
26 | * with this program; if not, write to the Free Software Foundation, Inc., |
27 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
28 | * http://www.gnu.org/copyleft/gpl.html |
29 | * |
30 | * @file |
31 | */ |
32 | |
33 | class EnhancedChangesList extends ChangesList { |
34 | |
35 | /** |
36 | * @var RCCacheEntryFactory |
37 | */ |
38 | protected $cacheEntryFactory; |
39 | |
40 | /** |
41 | * @var RCCacheEntry[][] |
42 | */ |
43 | protected $rc_cache; |
44 | |
45 | /** |
46 | * @var TemplateParser |
47 | */ |
48 | protected $templateParser; |
49 | |
50 | /** |
51 | * @param IContextSource $context |
52 | * @param ChangesListFilterGroup[] $filterGroups Array of ChangesListFilterGroup objects (currently optional) |
53 | */ |
54 | public function __construct( $context, array $filterGroups = [] ) { |
55 | parent::__construct( $context, $filterGroups ); |
56 | |
57 | // message is set by the parent ChangesList class |
58 | $this->cacheEntryFactory = new RCCacheEntryFactory( |
59 | $context, |
60 | $this->message, |
61 | $this->linkRenderer |
62 | ); |
63 | $this->templateParser = new TemplateParser(); |
64 | } |
65 | |
66 | /** |
67 | * Add the JavaScript file for enhanced changeslist |
68 | * @return string |
69 | */ |
70 | public function beginRecentChangesList() { |
71 | $this->getOutput()->addModuleStyles( [ |
72 | 'mediawiki.special.changeslist.enhanced', |
73 | ] ); |
74 | |
75 | parent::beginRecentChangesList(); |
76 | return '<div class="mw-changeslist" aria-live="polite">'; |
77 | } |
78 | |
79 | /** |
80 | * Format a line for enhanced recentchange (aka with javascript and block of lines). |
81 | * |
82 | * @param RecentChange &$rc |
83 | * @param bool $watched |
84 | * @param int|null $linenumber (default null) |
85 | * |
86 | * @return string |
87 | */ |
88 | public function recentChangesLine( &$rc, $watched = false, $linenumber = null ) { |
89 | $date = $this->getLanguage()->userDate( |
90 | $rc->mAttribs['rc_timestamp'], |
91 | $this->getUser() |
92 | ); |
93 | if ( $this->lastdate === '' ) { |
94 | $this->lastdate = $date; |
95 | } |
96 | |
97 | $ret = ''; |
98 | |
99 | # If it's a new day, flush the cache and update $this->lastdate |
100 | if ( $date !== $this->lastdate ) { |
101 | # Process current cache (uses $this->lastdate to generate a heading) |
102 | $ret = $this->recentChangesBlock(); |
103 | $this->rc_cache = []; |
104 | $this->lastdate = $date; |
105 | } |
106 | |
107 | $cacheEntry = $this->cacheEntryFactory->newFromRecentChange( $rc, $watched ); |
108 | $this->addCacheEntry( $cacheEntry ); |
109 | |
110 | return $ret; |
111 | } |
112 | |
113 | /** |
114 | * Put accumulated information into the cache, for later display. |
115 | * Page moves go on their own line. |
116 | * |
117 | * @param RCCacheEntry $cacheEntry |
118 | */ |
119 | protected function addCacheEntry( RCCacheEntry $cacheEntry ) { |
120 | $cacheGroupingKey = $this->makeCacheGroupingKey( $cacheEntry ); |
121 | $this->rc_cache[$cacheGroupingKey][] = $cacheEntry; |
122 | } |
123 | |
124 | /** |
125 | * @todo use rc_source to group, if set; fallback to rc_type |
126 | * |
127 | * @param RCCacheEntry $cacheEntry |
128 | * |
129 | * @return string |
130 | */ |
131 | protected function makeCacheGroupingKey( RCCacheEntry $cacheEntry ) { |
132 | $title = $cacheEntry->getTitle(); |
133 | $cacheGroupingKey = $title->getPrefixedDBkey(); |
134 | |
135 | $type = $cacheEntry->mAttribs['rc_type']; |
136 | |
137 | if ( $type == RC_LOG ) { |
138 | // Group by log type |
139 | $cacheGroupingKey = SpecialPage::getTitleFor( |
140 | 'Log', |
141 | $cacheEntry->mAttribs['rc_log_type'] |
142 | )->getPrefixedDBkey(); |
143 | } |
144 | |
145 | return $cacheGroupingKey; |
146 | } |
147 | |
148 | /** |
149 | * Enhanced RC group |
150 | * @param RCCacheEntry[] $block |
151 | * @return string |
152 | */ |
153 | protected function recentChangesBlockGroup( $block ) { |
154 | $recentChangesFlags = $this->getConfig()->get( MainConfigNames::RecentChangesFlags ); |
155 | |
156 | # Add the namespace and title of the block as part of the class |
157 | $tableClasses = [ 'mw-enhanced-rc', 'mw-changeslist-line' ]; |
158 | if ( $block[0]->mAttribs['rc_log_type'] ) { |
159 | # Log entry |
160 | $tableClasses[] = 'mw-changeslist-log'; |
161 | $tableClasses[] = Sanitizer::escapeClass( 'mw-changeslist-log-' |
162 | . $block[0]->mAttribs['rc_log_type'] ); |
163 | } else { |
164 | $tableClasses[] = 'mw-changeslist-edit'; |
165 | $tableClasses[] = Sanitizer::escapeClass( 'mw-changeslist-ns' |
166 | . $block[0]->mAttribs['rc_namespace'] . '-' . $block[0]->mAttribs['rc_title'] ); |
167 | } |
168 | if ( $block[0]->watched ) { |
169 | $tableClasses[] = 'mw-changeslist-line-watched'; |
170 | } else { |
171 | $tableClasses[] = 'mw-changeslist-line-not-watched'; |
172 | } |
173 | |
174 | # Collate list of users |
175 | $usercounts = []; |
176 | $userlinks = []; |
177 | # Some catalyst variables... |
178 | $namehidden = true; |
179 | $allLogs = true; |
180 | $RCShowChangedSize = $this->getConfig()->get( MainConfigNames::RCShowChangedSize ); |
181 | |
182 | # Default values for RC flags |
183 | $collectedRcFlags = []; |
184 | foreach ( $recentChangesFlags as $key => $value ) { |
185 | $flagGrouping = $value['grouping'] ?? 'any'; |
186 | switch ( $flagGrouping ) { |
187 | case 'all': |
188 | $collectedRcFlags[$key] = true; |
189 | break; |
190 | case 'any': |
191 | $collectedRcFlags[$key] = false; |
192 | break; |
193 | default: |
194 | throw new DomainException( "Unknown grouping type \"{$flagGrouping}\"" ); |
195 | } |
196 | } |
197 | foreach ( $block as $rcObj ) { |
198 | // If all log actions to this page were hidden, then don't |
199 | // give the name of the affected page for this block! |
200 | if ( !static::isDeleted( $rcObj, LogPage::DELETED_ACTION ) ) { |
201 | $namehidden = false; |
202 | } |
203 | $username = $rcObj->getPerformerIdentity()->getName(); |
204 | $userlink = $rcObj->userlink; |
205 | if ( !isset( $usercounts[$username] ) ) { |
206 | $usercounts[$username] = 0; |
207 | $userlinks[$username] = $userlink; |
208 | } |
209 | if ( $rcObj->mAttribs['rc_type'] != RC_LOG ) { |
210 | $allLogs = false; |
211 | } |
212 | |
213 | $usercounts[$username]++; |
214 | } |
215 | |
216 | # Sort the list and convert to text |
217 | krsort( $usercounts ); |
218 | asort( $usercounts ); |
219 | $users = []; |
220 | foreach ( $usercounts as $username => $count ) { |
221 | $text = $userlinks[$username]; |
222 | $text .= $this->getLanguage()->getDirMark(); |
223 | if ( $count > 1 ) { |
224 | $formattedCount = $this->msg( 'ntimes' )->numParams( $count )->escaped(); |
225 | $text .= ' ' . $this->msg( 'parentheses' )->rawParams( $formattedCount )->escaped(); |
226 | } |
227 | $users[] = $text; |
228 | } |
229 | |
230 | # Article link |
231 | $articleLink = ''; |
232 | $revDeletedMsg = false; |
233 | if ( $namehidden ) { |
234 | $revDeletedMsg = $this->msg( 'rev-deleted-event' )->escaped(); |
235 | } elseif ( $allLogs ) { |
236 | $articleLink = $this->maybeWatchedLink( $block[0]->link, $block[0]->watched ); |
237 | } else { |
238 | $articleLink = $this->getArticleLink( |
239 | $block[0], $block[0]->unpatrolled, $block[0]->watched ); |
240 | } |
241 | |
242 | # Sub-entries |
243 | $lines = []; |
244 | $filterClasses = []; |
245 | foreach ( $block as $i => $rcObj ) { |
246 | $line = $this->getLineData( $block, $rcObj ); |
247 | if ( !$line ) { |
248 | // completely ignore this RC entry if we don't want to render it |
249 | unset( $block[$i] ); |
250 | continue; |
251 | } |
252 | |
253 | // Roll up flags |
254 | foreach ( $line['recentChangesFlagsRaw'] as $key => $value ) { |
255 | $flagGrouping = ( $recentChangesFlags[$key]['grouping'] ?? 'any' ); |
256 | switch ( $flagGrouping ) { |
257 | case 'all': |
258 | if ( !$value ) { |
259 | $collectedRcFlags[$key] = false; |
260 | } |
261 | break; |
262 | case 'any': |
263 | if ( $value ) { |
264 | $collectedRcFlags[$key] = true; |
265 | } |
266 | break; |
267 | default: |
268 | throw new DomainException( "Unknown grouping type \"{$flagGrouping}\"" ); |
269 | } |
270 | } |
271 | |
272 | // Roll up filter-based CSS classes |
273 | $filterClasses = array_merge( $filterClasses, $this->getHTMLClassesForFilters( $rcObj ) ); |
274 | // Add classes for change tags separately, getHTMLClassesForFilters() doesn't add them |
275 | $this->getTags( $rcObj, $filterClasses ); |
276 | $filterClasses = array_unique( $filterClasses ); |
277 | |
278 | $lines[] = $line; |
279 | } |
280 | |
281 | // Further down are some assumptions that $block is a 0-indexed array |
282 | // with (count-1) as last key. Let's make sure it is. |
283 | $block = array_values( $block ); |
284 | $filterClasses = array_values( $filterClasses ); |
285 | |
286 | if ( !$block || !$lines ) { |
287 | // if we can't show anything, don't display this block altogether |
288 | return ''; |
289 | } |
290 | |
291 | $logText = $this->getLogText( $block, [], $allLogs, |
292 | $collectedRcFlags['newpage'], $namehidden |
293 | ); |
294 | |
295 | # Character difference (does not apply if only log items) |
296 | $charDifference = false; |
297 | if ( $RCShowChangedSize && !$allLogs ) { |
298 | $last = 0; |
299 | $first = count( $block ) - 1; |
300 | # Some events (like logs and category changes) have an "empty" size, so we need to skip those... |
301 | while ( $last < $first && $block[$last]->mAttribs['rc_new_len'] === null ) { |
302 | $last++; |
303 | } |
304 | while ( $last < $first && $block[$first]->mAttribs['rc_old_len'] === null ) { |
305 | $first--; |
306 | } |
307 | # Get net change |
308 | $charDifference = $this->formatCharacterDifference( $block[$first], $block[$last] ) ?: false; |
309 | } |
310 | |
311 | $numberofWatchingusers = $this->numberofWatchingusers( $block[0]->numberofWatchingusers ); |
312 | $usersList = $this->msg( 'brackets' )->rawParams( |
313 | implode( $this->message['semicolon-separator'], $users ) |
314 | )->escaped(); |
315 | |
316 | $prefix = ''; |
317 | if ( is_callable( $this->changeLinePrefixer ) ) { |
318 | $prefix = call_user_func( $this->changeLinePrefixer, $block[0], $this, true ); |
319 | } |
320 | |
321 | $templateParams = [ |
322 | 'checkboxId' => 'mw-checkbox-' . base64_encode( random_bytes( 3 ) ), |
323 | 'articleLink' => $articleLink, |
324 | 'charDifference' => $charDifference, |
325 | 'collectedRcFlags' => $this->recentChangesFlags( $collectedRcFlags ), |
326 | 'filterClasses' => $filterClasses, |
327 | 'languageDirMark' => $this->getLanguage()->getDirMark(), |
328 | 'lines' => $lines, |
329 | 'logText' => $logText, |
330 | 'numberofWatchingusers' => $numberofWatchingusers, |
331 | 'prefix' => $prefix, |
332 | 'rev-deleted-event' => $revDeletedMsg, |
333 | 'tableClasses' => $tableClasses, |
334 | 'timestamp' => $block[0]->timestamp, |
335 | 'fullTimestamp' => $block[0]->getAttribute( 'rc_timestamp' ), |
336 | 'users' => $usersList, |
337 | ]; |
338 | |
339 | $this->rcCacheIndex++; |
340 | |
341 | return $this->templateParser->processTemplate( |
342 | 'EnhancedChangesListGroup', |
343 | $templateParams |
344 | ); |
345 | } |
346 | |
347 | /** |
348 | * @param RCCacheEntry[] $block |
349 | * @param RCCacheEntry $rcObj |
350 | * @param array $queryParams |
351 | * @return array |
352 | */ |
353 | protected function getLineData( array $block, RCCacheEntry $rcObj, array $queryParams = [] ) { |
354 | $RCShowChangedSize = $this->getConfig()->get( MainConfigNames::RCShowChangedSize ); |
355 | |
356 | $type = $rcObj->mAttribs['rc_type']; |
357 | $data = []; |
358 | $lineParams = [ 'targetTitle' => $rcObj->getTitle() ]; |
359 | |
360 | $classes = [ 'mw-enhanced-rc' ]; |
361 | if ( $rcObj->watched ) { |
362 | $classes[] = 'mw-enhanced-watched'; |
363 | } |
364 | $classes = array_merge( $classes, $this->getHTMLClasses( $rcObj, $rcObj->watched ) ); |
365 | |
366 | $separator = ' <span class="mw-changeslist-separator"></span> '; |
367 | |
368 | $data['recentChangesFlags'] = [ |
369 | 'newpage' => $type == RC_NEW, |
370 | 'minor' => $rcObj->mAttribs['rc_minor'], |
371 | 'unpatrolled' => $rcObj->unpatrolled, |
372 | 'bot' => $rcObj->mAttribs['rc_bot'], |
373 | ]; |
374 | |
375 | # Log timestamp |
376 | if ( $type == RC_LOG ) { |
377 | $link = htmlspecialchars( $rcObj->timestamp ); |
378 | # Revision link |
379 | } elseif ( !ChangesList::userCan( $rcObj, RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) { |
380 | $link = Html::element( 'span', [ 'class' => 'history-deleted' ], $rcObj->timestamp ); |
381 | } else { |
382 | $params = []; |
383 | $params['curid'] = $rcObj->mAttribs['rc_cur_id']; |
384 | if ( $rcObj->mAttribs['rc_this_oldid'] != 0 ) { |
385 | $params['oldid'] = $rcObj->mAttribs['rc_this_oldid']; |
386 | } |
387 | // FIXME: The link has incorrect "title=" when rc_type = RC_CATEGORIZE. |
388 | // rc_cur_id refers to the page that was categorized |
389 | // whereas RecentChange::getTitle refers to the category. |
390 | $link = $this->linkRenderer->makeKnownLink( |
391 | $rcObj->getTitle(), |
392 | $rcObj->timestamp, |
393 | [], |
394 | $params + $queryParams |
395 | ); |
396 | if ( static::isDeleted( $rcObj, RevisionRecord::DELETED_TEXT ) ) { |
397 | $link = '<span class="history-deleted">' . $link . '</span> '; |
398 | } |
399 | } |
400 | $data['timestampLink'] = $link; |
401 | |
402 | $currentAndLastLinks = ''; |
403 | if ( $type == RC_EDIT || $type == RC_NEW ) { |
404 | $currentAndLastLinks .= ' ' . $this->msg( 'parentheses' )->rawParams( |
405 | $rcObj->curlink . |
406 | $this->message['pipe-separator'] . |
407 | $rcObj->lastlink |
408 | )->escaped(); |
409 | } |
410 | $data['currentAndLastLinks'] = $currentAndLastLinks; |
411 | $data['separatorAfterCurrentAndLastLinks'] = $separator; |
412 | |
413 | # Character diff |
414 | if ( $RCShowChangedSize ) { |
415 | $cd = $this->formatCharacterDifference( $rcObj ); |
416 | if ( $cd !== '' ) { |
417 | $data['characterDiff'] = $cd; |
418 | $data['separatorAfterCharacterDiff'] = $separator; |
419 | } |
420 | } |
421 | |
422 | if ( $type == RC_LOG ) { |
423 | $data['logEntry'] = $this->insertLogEntry( $rcObj ); |
424 | } elseif ( $this->isCategorizationWithoutRevision( $rcObj ) ) { |
425 | $data['comment'] = $this->insertComment( $rcObj ); |
426 | } else { |
427 | # User links |
428 | $data['userLink'] = $rcObj->userlink; |
429 | $data['userTalkLink'] = $rcObj->usertalklink; |
430 | $data['comment'] = $this->insertComment( $rcObj ); |
431 | if ( $type == RC_CATEGORIZE ) { |
432 | $data['historyLink'] = $this->getDiffHistLinks( $rcObj, false ); |
433 | } |
434 | # Rollback, thanks etc... |
435 | $data['rollback'] = $this->getRollback( $rcObj ); |
436 | } |
437 | |
438 | # Tags |
439 | $data['tags'] = $this->getTags( $rcObj, $classes ); |
440 | |
441 | $attribs = $this->getDataAttributes( $rcObj ); |
442 | |
443 | // give the hook a chance to modify the data |
444 | $success = $this->getHookRunner()->onEnhancedChangesListModifyLineData( |
445 | $this, $data, $block, $rcObj, $classes, $attribs ); |
446 | if ( !$success ) { |
447 | // skip entry if hook aborted it |
448 | return []; |
449 | } |
450 | $attribs = array_filter( $attribs, |
451 | [ Sanitizer::class, 'isReservedDataAttribute' ], |
452 | ARRAY_FILTER_USE_KEY |
453 | ); |
454 | |
455 | $lineParams['recentChangesFlagsRaw'] = []; |
456 | if ( isset( $data['recentChangesFlags'] ) ) { |
457 | $lineParams['recentChangesFlags'] = $this->recentChangesFlags( $data['recentChangesFlags'] ); |
458 | # FIXME: This is used by logic, don't return it in the template params. |
459 | $lineParams['recentChangesFlagsRaw'] = $data['recentChangesFlags']; |
460 | unset( $data['recentChangesFlags'] ); |
461 | } |
462 | |
463 | if ( isset( $data['timestampLink'] ) ) { |
464 | $lineParams['timestampLink'] = $data['timestampLink']; |
465 | unset( $data['timestampLink'] ); |
466 | } |
467 | |
468 | $lineParams['classes'] = array_values( $classes ); |
469 | $lineParams['attribs'] = Html::expandAttributes( $attribs ); |
470 | |
471 | // everything else: makes it easier for extensions to add or remove data |
472 | $lineParams['data'] = array_values( $data ); |
473 | |
474 | return $lineParams; |
475 | } |
476 | |
477 | /** |
478 | * Generates amount of changes (linking to diff ) & link to history. |
479 | * |
480 | * @param RCCacheEntry[] $block |
481 | * @param array $queryParams |
482 | * @param bool $allLogs |
483 | * @param bool $isnew |
484 | * @param bool $namehidden |
485 | * @return string |
486 | */ |
487 | protected function getLogText( $block, $queryParams, $allLogs, $isnew, $namehidden ) { |
488 | if ( !$block ) { |
489 | return ''; |
490 | } |
491 | |
492 | // Changes message |
493 | static $nchanges = []; |
494 | static $sinceLastVisitMsg = []; |
495 | |
496 | $n = count( $block ); |
497 | if ( !isset( $nchanges[$n] ) ) { |
498 | $nchanges[$n] = $this->msg( 'nchanges' )->numParams( $n )->escaped(); |
499 | } |
500 | |
501 | $sinceLast = 0; |
502 | $unvisitedOldid = null; |
503 | $currentRevision = 0; |
504 | $previousRevision = 0; |
505 | $curId = 0; |
506 | $allCategorization = true; |
507 | /** @var RCCacheEntry $rcObj */ |
508 | foreach ( $block as $rcObj ) { |
509 | // Fields of categorization entries refer to the changed page |
510 | // rather than the category for which we are building the log text. |
511 | if ( $rcObj->mAttribs['rc_type'] == RC_CATEGORIZE ) { |
512 | continue; |
513 | } |
514 | |
515 | $allCategorization = false; |
516 | $previousRevision = $rcObj->mAttribs['rc_last_oldid']; |
517 | // Same logic as below inside main foreach |
518 | if ( $rcObj->watched ) { |
519 | $sinceLast++; |
520 | $unvisitedOldid = $previousRevision; |
521 | } |
522 | if ( !$currentRevision ) { |
523 | $currentRevision = $rcObj->mAttribs['rc_this_oldid']; |
524 | } |
525 | if ( !$curId ) { |
526 | $curId = $rcObj->mAttribs['rc_cur_id']; |
527 | } |
528 | } |
529 | |
530 | // Total change link |
531 | $links = []; |
532 | $title = $block[0]->getTitle(); |
533 | if ( !$allLogs ) { |
534 | // TODO: Disable the link if the user cannot see it (rc_deleted). |
535 | // Beware of possibly interspersed categorization entries. |
536 | if ( $isnew || $allCategorization ) { |
537 | $links['total-changes'] = Html::rawElement( 'span', [], $nchanges[$n] ); |
538 | } else { |
539 | $links['total-changes'] = Html::rawElement( 'span', [], |
540 | $this->linkRenderer->makeKnownLink( |
541 | $title, |
542 | new HtmlArmor( $nchanges[$n] ), |
543 | [ 'class' => 'mw-changeslist-groupdiff' ], |
544 | $queryParams + [ |
545 | 'curid' => $curId, |
546 | 'diff' => $currentRevision, |
547 | 'oldid' => $previousRevision, |
548 | ] |
549 | ) |
550 | ); |
551 | } |
552 | |
553 | if ( |
554 | !$allCategorization && |
555 | $sinceLast > 0 && |
556 | $sinceLast < $n |
557 | ) { |
558 | if ( !isset( $sinceLastVisitMsg[$sinceLast] ) ) { |
559 | $sinceLastVisitMsg[$sinceLast] = |
560 | $this->msg( 'enhancedrc-since-last-visit' )->numParams( $sinceLast )->escaped(); |
561 | } |
562 | $links['total-changes-since-last'] = Html::rawElement( 'span', [], |
563 | $this->linkRenderer->makeKnownLink( |
564 | $title, |
565 | new HtmlArmor( $sinceLastVisitMsg[$sinceLast] ), |
566 | [ 'class' => 'mw-changeslist-groupdiff' ], |
567 | $queryParams + [ |
568 | 'curid' => $curId, |
569 | 'diff' => $currentRevision, |
570 | 'oldid' => $unvisitedOldid, |
571 | ] |
572 | ) |
573 | ); |
574 | } |
575 | } |
576 | |
577 | // History |
578 | if ( $allLogs || $allCategorization ) { |
579 | // don't show history link for logs |
580 | } elseif ( $namehidden || !$title->exists() ) { |
581 | $links['history'] = Html::rawElement( 'span', [], $this->message['enhancedrc-history'] ); |
582 | } else { |
583 | $links['history'] = Html::rawElement( 'span', [], |
584 | $this->linkRenderer->makeKnownLink( |
585 | $title, |
586 | new HtmlArmor( $this->message['enhancedrc-history'] ), |
587 | [ 'class' => 'mw-changeslist-history' ], |
588 | [ |
589 | 'curid' => $curId, |
590 | 'action' => 'history', |
591 | ] + $queryParams |
592 | ) |
593 | ); |
594 | } |
595 | |
596 | // Allow others to alter, remove or add to these links |
597 | $this->getHookRunner()->onEnhancedChangesList__getLogText( $this, $links, $block ); |
598 | |
599 | if ( !$links ) { |
600 | return ''; |
601 | } |
602 | |
603 | $logtext = Html::rawElement( 'span', [ 'class' => 'mw-changeslist-links' ], |
604 | implode( ' ', $links ) ); |
605 | return ' ' . $logtext; |
606 | } |
607 | |
608 | /** |
609 | * Enhanced RC ungrouped line. |
610 | * |
611 | * @param RecentChange|RCCacheEntry $rcObj |
612 | * @return string A HTML formatted line (generated using $r) |
613 | */ |
614 | protected function recentChangesBlockLine( $rcObj ) { |
615 | $data = []; |
616 | |
617 | $type = $rcObj->mAttribs['rc_type']; |
618 | $logType = $rcObj->mAttribs['rc_log_type']; |
619 | $classes = $this->getHTMLClasses( $rcObj, $rcObj->watched ); |
620 | $classes[] = 'mw-enhanced-rc'; |
621 | |
622 | if ( $logType ) { |
623 | # Log entry |
624 | $classes[] = 'mw-changeslist-log'; |
625 | $classes[] = Sanitizer::escapeClass( 'mw-changeslist-log-' . $logType ); |
626 | } else { |
627 | $classes[] = 'mw-changeslist-edit'; |
628 | $classes[] = Sanitizer::escapeClass( 'mw-changeslist-ns' . |
629 | $rcObj->mAttribs['rc_namespace'] . '-' . $rcObj->mAttribs['rc_title'] ); |
630 | } |
631 | |
632 | # Flag and Timestamp |
633 | $data['recentChangesFlags'] = [ |
634 | 'newpage' => $type == RC_NEW, |
635 | 'minor' => $rcObj->mAttribs['rc_minor'], |
636 | 'unpatrolled' => $rcObj->unpatrolled, |
637 | 'bot' => $rcObj->mAttribs['rc_bot'], |
638 | ]; |
639 | // timestamp is not really a link here, but is called timestampLink |
640 | // for consistency with EnhancedChangesListModifyLineData |
641 | $data['timestampLink'] = htmlspecialchars( $rcObj->timestamp ); |
642 | |
643 | # Article or log link |
644 | if ( $logType ) { |
645 | $logPage = new LogPage( $logType ); |
646 | $logTitle = SpecialPage::getTitleFor( 'Log', $logType ); |
647 | $logName = $logPage->getName()->text(); |
648 | $data['logLink'] = Html::rawElement( 'span', [ 'class' => 'mw-changeslist-links' ], |
649 | $this->linkRenderer->makeKnownLink( $logTitle, $logName ) |
650 | ); |
651 | } else { |
652 | $data['articleLink'] = $this->getArticleLink( $rcObj, $rcObj->unpatrolled, $rcObj->watched ); |
653 | } |
654 | |
655 | # Diff and hist links |
656 | if ( $type != RC_LOG && $type != RC_CATEGORIZE ) { |
657 | $data['historyLink'] = $this->getDiffHistLinks( $rcObj, false ); |
658 | } |
659 | $data['separatorAfterLinks'] = ' <span class="mw-changeslist-separator"></span> '; |
660 | |
661 | # Character diff |
662 | if ( $this->getConfig()->get( MainConfigNames::RCShowChangedSize ) ) { |
663 | $cd = $this->formatCharacterDifference( $rcObj ); |
664 | if ( $cd !== '' ) { |
665 | $data['characterDiff'] = $cd; |
666 | $data['separatorAftercharacterDiff'] = ' <span class="mw-changeslist-separator"></span> '; |
667 | } |
668 | } |
669 | |
670 | if ( $type == RC_LOG ) { |
671 | $data['logEntry'] = $this->insertLogEntry( $rcObj ); |
672 | } elseif ( $this->isCategorizationWithoutRevision( $rcObj ) ) { |
673 | $data['comment'] = $this->insertComment( $rcObj ); |
674 | } else { |
675 | $data['userLink'] = $rcObj->userlink; |
676 | $data['userTalkLink'] = $rcObj->usertalklink; |
677 | $data['comment'] = $this->insertComment( $rcObj ); |
678 | if ( $type == RC_CATEGORIZE ) { |
679 | $data['historyLink'] = $this->getDiffHistLinks( $rcObj, false ); |
680 | } |
681 | $data['rollback'] = $this->getRollback( $rcObj ); |
682 | } |
683 | |
684 | # Tags |
685 | $data['tags'] = $this->getTags( $rcObj, $classes ); |
686 | |
687 | # Show how many people are watching this if enabled |
688 | $data['watchingUsers'] = $this->numberofWatchingusers( $rcObj->numberofWatchingusers ); |
689 | |
690 | $data['attribs'] = array_merge( $this->getDataAttributes( $rcObj ), [ 'class' => $classes ] ); |
691 | |
692 | // give the hook a chance to modify the data |
693 | $success = $this->getHookRunner()->onEnhancedChangesListModifyBlockLineData( |
694 | $this, $data, $rcObj ); |
695 | if ( !$success ) { |
696 | // skip entry if hook aborted it |
697 | return ''; |
698 | } |
699 | $attribs = $data['attribs']; |
700 | unset( $data['attribs'] ); |
701 | $attribs = array_filter( $attribs, static function ( $key ) { |
702 | return $key === 'class' || Sanitizer::isReservedDataAttribute( $key ); |
703 | }, ARRAY_FILTER_USE_KEY ); |
704 | |
705 | $prefix = ''; |
706 | if ( is_callable( $this->changeLinePrefixer ) ) { |
707 | $prefix = call_user_func( $this->changeLinePrefixer, $rcObj, $this, false ); |
708 | } |
709 | |
710 | $line = Html::openElement( 'table', $attribs ) . Html::openElement( 'tr' ); |
711 | // Highlight block |
712 | $line .= Html::rawElement( 'td', [], |
713 | $this->getHighlightsContainerDiv() |
714 | ); |
715 | |
716 | $line .= Html::rawElement( 'td', [], '<span class="mw-enhancedchanges-arrow-space"></span>' ); |
717 | $line .= Html::rawElement( 'td', [ 'class' => 'mw-changeslist-line-prefix' ], $prefix ); |
718 | $line .= '<td class="mw-enhanced-rc" colspan="2">'; |
719 | |
720 | if ( isset( $data['recentChangesFlags'] ) ) { |
721 | $line .= $this->recentChangesFlags( $data['recentChangesFlags'] ); |
722 | unset( $data['recentChangesFlags'] ); |
723 | } |
724 | |
725 | if ( isset( $data['timestampLink'] ) ) { |
726 | $line .= "\u{00A0}" . $data['timestampLink']; |
727 | unset( $data['timestampLink'] ); |
728 | } |
729 | $line .= "\u{00A0}</td>"; |
730 | $line .= Html::openElement( 'td', [ |
731 | 'class' => 'mw-changeslist-line-inner', |
732 | // Used for reliable determination of the affiliated page |
733 | 'data-target-page' => $rcObj->getTitle(), |
734 | ] ); |
735 | |
736 | // everything else: makes it easier for extensions to add or remove data |
737 | foreach ( $data as $key => $dataItem ) { |
738 | $line .= Html::rawElement( 'span', [ |
739 | 'class' => 'mw-changeslist-line-inner-' . $key, |
740 | ], $dataItem ); |
741 | } |
742 | |
743 | $line .= "</td></tr></table>\n"; |
744 | |
745 | return $line; |
746 | } |
747 | |
748 | /** |
749 | * Returns value to be used in 'historyLink' element of $data param in |
750 | * EnhancedChangesListModifyBlockLineData hook. |
751 | * |
752 | * @since 1.27 |
753 | * |
754 | * @param RCCacheEntry $rc |
755 | * @param bool|array|null $query deprecated |
756 | * @param bool|null $useParentheses (optional) Wrap comments in parentheses where needed |
757 | * @return string HTML |
758 | */ |
759 | public function getDiffHistLinks( RCCacheEntry $rc, $query = null, $useParentheses = null ) { |
760 | if ( is_bool( $query ) ) { |
761 | $useParentheses = $query; |
762 | } elseif ( $query !== null ) { |
763 | wfDeprecated( __METHOD__ . ' with $query parameter', '1.36' ); |
764 | } |
765 | $pageTitle = $rc->getTitle(); |
766 | if ( $rc->getAttribute( 'rc_type' ) == RC_CATEGORIZE ) { |
767 | // For categorizations we must swap the category title with the page title! |
768 | $pageTitle = Title::newFromID( $rc->getAttribute( 'rc_cur_id' ) ); |
769 | if ( !$pageTitle ) { |
770 | // The page has been deleted, but the RC entry |
771 | // deletion job has not run yet. Just skip. |
772 | return ''; |
773 | } |
774 | } |
775 | |
776 | $histLink = $this->linkRenderer->makeKnownLink( |
777 | $pageTitle, |
778 | new HtmlArmor( $this->message['hist'] ), |
779 | [ 'class' => 'mw-changeslist-history' ], |
780 | [ |
781 | 'curid' => $rc->getAttribute( 'rc_cur_id' ), |
782 | 'action' => 'history' |
783 | ] |
784 | ); |
785 | if ( $useParentheses !== false ) { |
786 | $retVal = $this->msg( 'parentheses' ) |
787 | ->rawParams( $rc->difflink . $this->message['pipe-separator'] |
788 | . $histLink )->escaped(); |
789 | } else { |
790 | $retVal = Html::rawElement( 'span', [ 'class' => 'mw-changeslist-links' ], |
791 | Html::rawElement( 'span', [], $rc->difflink ) . |
792 | Html::rawElement( 'span', [], $histLink ) |
793 | ); |
794 | } |
795 | return ' ' . $retVal; |
796 | } |
797 | |
798 | /** |
799 | * If enhanced RC is in use, this function takes the previously cached |
800 | * RC lines, arranges them, and outputs the HTML |
801 | * |
802 | * @return string |
803 | */ |
804 | protected function recentChangesBlock() { |
805 | if ( count( $this->rc_cache ) == 0 ) { |
806 | return ''; |
807 | } |
808 | |
809 | $blockOut = ''; |
810 | foreach ( $this->rc_cache as $block ) { |
811 | if ( count( $block ) < 2 ) { |
812 | $blockOut .= $this->recentChangesBlockLine( array_shift( $block ) ); |
813 | } else { |
814 | $blockOut .= $this->recentChangesBlockGroup( $block ); |
815 | } |
816 | } |
817 | |
818 | if ( $blockOut === '' ) { |
819 | return ''; |
820 | } |
821 | // $this->lastdate is kept up to date by recentChangesLine() |
822 | return Html::element( 'h4', [], $this->lastdate ) . "\n<div>" . $blockOut . '</div>'; |
823 | } |
824 | |
825 | /** |
826 | * Returns text for the end of RC |
827 | * If enhanced RC is in use, returns pretty much all the text |
828 | * @return string |
829 | */ |
830 | public function endRecentChangesList() { |
831 | return $this->recentChangesBlock() . '</div>'; |
832 | } |
833 | } |