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