MediaWiki master
RightsLogFormatter.php
Go to the documentation of this file.
1<?php
12namespace MediaWiki\Logging;
13
20
29 protected function getCommentWikiId() {
30 // If the performer is hidden, don't attempt to resolve the source wiki
31 // to avoid leaking information about the performer's origin.
32 if ( !$this->canView( LogPage::DELETED_USER ) ) {
33 return false;
34 }
35 $performerName = $this->entry->getPerformerIdentity()->getName();
36 // External performers have the format "wikiId>username".
37 // The wiki ID prefix indicates the wiki the log entry was
38 // replicated from, and comment links should resolve against it.
39 return ExternalUserNames::getPrefix( $performerName );
40 }
41
43 protected function makePageLink( ?Title $title = null, $parameters = [], $html = null ) {
44 $userrightsInterwikiDelimiter = $this->context->getConfig()
46
47 if ( !$this->plaintext ) {
48 $text = $this->getContentLanguage()->
49 ucfirst( $title->getDBkey() );
50 $parts = explode( $userrightsInterwikiDelimiter, $text, 2 );
51
52 if ( count( $parts ) === 2 ) {
53 // @phan-suppress-next-line SecurityCheck-DoubleEscaped
54 $titleLink = WikiMap::foreignUserLink(
55 $parts[1],
56 $parts[0],
57 htmlspecialchars(
58 strtr( $parts[0], '_', ' ' ) .
59 $userrightsInterwikiDelimiter .
60 $parts[1]
61 )
62 );
63
64 if ( $titleLink !== false ) {
65 return $titleLink;
66 }
67 }
68 }
69
70 return parent::makePageLink( $title, $parameters, $title ? $title->getText() : null );
71 }
72
74 protected function getMessageKey() {
75 $key = parent::getMessageKey();
76 $params = $this->getMessageParameters();
77 if ( !isset( $params[3] ) && !isset( $params[4] ) ) {
78 // Messages: logentry-rights-rights-legacy
79 $key .= '-legacy';
80 }
81
82 return $key;
83 }
84
86 protected function getMessageParameters() {
87 $params = parent::getMessageParameters();
88
89 // Really old entries that lack old/new groups,
90 // so don't try to process them
91 if ( !$this->shouldProcessParams( $params ) ) {
92 return $params;
93 }
94
95 // Groups are stored as [ name => expiry|null ]
96 $oldGroups = $this->getOldGroups( $params );
97 $newGroups = $this->getNewGroups( $params );
98
99 // These params were used in the past, when the log message said "from: X to: Y"
100 // They were kept not to break translations
101 if ( count( $oldGroups ) ) {
102 $params[3] = Message::rawParam( $this->formatRightsList( $oldGroups ) );
103 } else {
104 $params[3] = $this->msg( 'rightsnone' )->text();
105 }
106 if ( count( $newGroups ) ) {
107 $params[4] = Message::rawParam( $this->formatRightsList( $newGroups ) );
108 } else {
109 $params[4] = $this->msg( 'rightsnone' )->text();
110 }
111
112 $performerName = $params[1];
113 $userName = $this->entry->getTarget()->getText();
114
115 $params[5] = $userName;
116
117 $groupChanges = $this->classifyGroupChanges( $oldGroups, $newGroups );
118
119 // The following messages are used here:
120 // * logentry-rights-rights-granted
121 // * logentry-rights-rights-revoked
122 // * logentry-rights-rights-expiry-changed
123 // * logentry-rights-rights-kept
124 // * logentry-rights-autopromote-granted
125 // * logentry-rights-autopromote-revoked
126 // * logentry-rights-autopromote-expiry-changed
127 // * logentry-rights-autopromote-kept
128 $messagePrefix = 'logentry-rights-rights-';
129 if ( $this->entry->getSubtype() === 'autopromote' ) {
130 $messagePrefix = 'logentry-rights-autopromote-';
131 }
132
133 $params[6] = $this->formatChangesToGroups( $groupChanges, $performerName, $userName,
134 $messagePrefix );
135
136 return $params;
137 }
138
148 protected function shouldProcessParams( array $params ) {
149 return isset( $params[3] ) || isset( $params[4] );
150 }
151
161 protected function getOldGroups( array $params ) {
162 if ( !isset( $params[3] ) ) {
163 return [];
164 }
165
166 $allParams = $this->entry->getParameters();
167 return $this->joinGroupsWithExpiries( $params[3], $allParams['oldmetadata'] ?? [] );
168 }
169
179 protected function getNewGroups( array $params ) {
180 if ( !isset( $params[4] ) ) {
181 return [];
182 }
183
184 $allParams = $this->entry->getParameters();
185 return $this->joinGroupsWithExpiries( $params[4], $allParams['newmetadata'] ?? [] );
186 }
187
200 protected function joinGroupsWithExpiries( $groupNames, array $metadata ) {
201 $groupNames = $this->makeGroupArray( $groupNames );
202 if ( !$this->plaintext && count( $groupNames ) ) {
203 $this->replaceGroupsWithMemberNames( $groupNames );
204 }
205
206 $expiries = [];
207 foreach (
208 array_map( null, $groupNames, $metadata )
209 as [ $groupName, $groupMetadata ]
210 ) {
211 if ( isset( $groupMetadata['expiry'] ) ) {
212 $expiry = $groupMetadata['expiry'];
213 } else {
214 $expiry = null;
215 }
216 $expiries[$groupName] = $expiry;
217 }
218 return $expiries;
219 }
220
228 protected function replaceGroupsWithMemberNames( array &$groupNames ) {
229 $lang = $this->context->getLanguage();
230 $userName = $this->entry->getTarget()->getText();
231 foreach ( $groupNames as &$group ) {
232 $group = $lang->getGroupMemberName( $group, $userName );
233 }
234 }
235
251 protected function classifyGroupChanges( array $oldGroups, array $newGroups ) {
252 $granted = array_diff_key( $newGroups, $oldGroups );
253 $revoked = array_diff_key( $oldGroups, $newGroups );
254 $kept = array_intersect_key( $oldGroups, $newGroups );
255
256 $expiryChanged = [];
257 $noChange = [];
258
259 foreach ( $kept as $group => $oldExpiry ) {
260 $newExpiry = $newGroups[$group];
261 if ( $oldExpiry !== $newExpiry ) {
262 $expiryChanged[$group] = [ $oldExpiry, $newExpiry ];
263 } else {
264 $noChange[$group] = $oldExpiry;
265 }
266 }
267
268 // These contain both group names and their expiry times
269 // in case of 'expiry-changed', the times are in an array [ old, new ]
270 return [
271 'granted' => $granted,
272 'revoked' => $revoked,
273 'expiry-changed' => $expiryChanged,
274 'kept' => $noChange,
275 ];
276 }
277
288 protected function formatChangesToGroups( array $groupChanges, string $performerName,
289 string $targetName, string $messagePrefix = 'logentry-rights-rights-'
290 ) {
291 $formattedChanges = [];
292
293 foreach ( $groupChanges as $changeType => $groups ) {
294 if ( !count( $groups ) ) {
295 continue;
296 }
297
298 if ( $changeType === 'expiry-changed' ) {
299 $formattedList = $this->formatRightsListExpiryChanged( $groups );
300 } else {
301 $formattedList = $this->formatRightsList( $groups );
302 }
303
304 $formattedChanges[] = $this->msg(
305 $messagePrefix . $changeType,
306 $formattedList,
307 $performerName,
308 $targetName
309 );
310 }
311
312 $uiLanguage = $this->context->getLanguage();
313 return $uiLanguage->semicolonList( $formattedChanges );
314 }
315
316 private function formatRightsListExpiryChanged( array $groups ): string {
317 $list = [];
318
319 foreach ( $groups as $group => [ $oldExpiry, $newExpiry ] ) {
320 $oldExpiryFormatted = $oldExpiry ? $this->formatDate( $oldExpiry ) : false;
321 $newExpiryFormatted = $newExpiry ? $this->formatDate( $newExpiry ) : false;
322
323 if ( $oldExpiryFormatted && $newExpiryFormatted ) {
324 // The expiration was changed
325 $list[] = $this->msg( 'rightslogentry-expiry-changed' )->params(
326 $group,
327 $newExpiryFormatted['whole'],
328 $newExpiryFormatted['date'],
329 $newExpiryFormatted['time'],
330 $oldExpiryFormatted['whole'],
331 $oldExpiryFormatted['date'],
332 $oldExpiryFormatted['time']
333 )->parse();
334 } elseif ( $oldExpiryFormatted ) {
335 // The expiration was removed
336 $list[] = $this->msg( 'rightslogentry-expiry-removed' )->params(
337 $group,
338 $oldExpiryFormatted['whole'],
339 $oldExpiryFormatted['date'],
340 $oldExpiryFormatted['time']
341 )->parse();
342 } elseif ( $newExpiryFormatted ) {
343 // The expiration was added
344 $list[] = $this->msg( 'rightslogentry-expiry-set' )->params(
345 $group,
346 $newExpiryFormatted['whole'],
347 $newExpiryFormatted['date'],
348 $newExpiryFormatted['time']
349 )->parse();
350 } else {
351 // The rights are and were permanent
352 // Shouldn't happen as we process only changes to expiry time here
353 $list[] = htmlspecialchars( $group );
354 }
355 }
356
357 $uiLanguage = $this->context->getLanguage();
358 return $uiLanguage->listToText( $list );
359 }
360
361 private function formatRightsList( array $groups ): string {
362 $uiLanguage = $this->context->getLanguage();
363 // separate arrays of temporary and permanent memberships
364 $tempList = $permList = [];
365
366 foreach ( $groups as $group => $expiry ) {
367 if ( $expiry ) {
368 // format the group and expiry into a friendly string
369 $expiryFormatted = $this->formatDate( $expiry );
370 $tempList[] = $this->msg( 'rightslogentry-temporary-group' )->params( $group,
371 $expiryFormatted['whole'], $expiryFormatted['date'], $expiryFormatted['time'] )
372 ->parse();
373 } else {
374 // the right does not expire; just insert the group name
375 $permList[] = htmlspecialchars( $group );
376 }
377 }
378
379 // place all temporary memberships first, to avoid the ambiguity of
380 // "administrator, bureaucrat and importer (temporary, until X time)"
381 return $uiLanguage->listToText( array_merge( $tempList, $permList ) );
382 }
383
384 private function formatDate( string $date ): array {
385 $uiLanguage = $this->context->getLanguage();
386 $uiUser = $this->context->getUser();
387
388 return [
389 'whole' => $uiLanguage->userTimeAndDate( $date, $uiUser ),
390 'date' => $uiLanguage->userDate( $date, $uiUser ),
391 'time' => $uiLanguage->userTime( $date, $uiUser ),
392 ];
393 }
394
396 protected function getParametersForApi() {
397 $entry = $this->entry;
398 $params = $entry->getParameters();
399
400 static $map = [
401 '4:array:oldgroups',
402 '5:array:newgroups',
403 '4::oldgroups' => '4:array:oldgroups',
404 '5::newgroups' => '5:array:newgroups',
405 ];
406 foreach ( $map as $index => $key ) {
407 if ( isset( $params[$index] ) ) {
408 $params[$key] = $params[$index];
409 unset( $params[$index] );
410 }
411 }
412
413 // Really old entries do not have log params, so form them from whatever info
414 // we have.
415 // Also walk through the parallel arrays of groups and metadata, combining each
416 // metadata array with the name of the group it pertains to
417 if ( isset( $params['4:array:oldgroups'] ) ) {
418 $params['4:array:oldgroups'] = $this->makeGroupArray( $params['4:array:oldgroups'] );
419
420 $oldmetadata =& $params['oldmetadata'];
421 // unset old metadata entry to ensure metadata goes at the end of the params array
422 unset( $params['oldmetadata'] );
423 $params['oldmetadata'] = array_map( static function ( $index ) use ( $params, $oldmetadata ) {
424 $result = [ 'group' => $params['4:array:oldgroups'][$index] ];
425 if ( isset( $oldmetadata[$index] ) ) {
426 $result += $oldmetadata[$index];
427 }
428 $result['expiry'] = ApiResult::formatExpiry( $result['expiry'] ?? null );
429
430 return $result;
431 }, array_keys( $params['4:array:oldgroups'] ) );
432 }
433
434 if ( isset( $params['5:array:newgroups'] ) ) {
435 $params['5:array:newgroups'] = $this->makeGroupArray( $params['5:array:newgroups'] );
436
437 $newmetadata =& $params['newmetadata'];
438 // unset old metadata entry to ensure metadata goes at the end of the params array
439 unset( $params['newmetadata'] );
440 $params['newmetadata'] = array_map( static function ( $index ) use ( $params, $newmetadata ) {
441 $result = [ 'group' => $params['5:array:newgroups'][$index] ];
442 if ( isset( $newmetadata[$index] ) ) {
443 $result += $newmetadata[$index];
444 }
445 $result['expiry'] = ApiResult::formatExpiry( $result['expiry'] ?? null );
446
447 return $result;
448 }, array_keys( $params['5:array:newgroups'] ) );
449 }
450
451 return $params;
452 }
453
455 public function formatParametersForApi() {
456 $ret = parent::formatParametersForApi();
457 if ( isset( $ret['oldgroups'] ) ) {
458 ApiResult::setIndexedTagName( $ret['oldgroups'], 'g' );
459 }
460 if ( isset( $ret['newgroups'] ) ) {
461 ApiResult::setIndexedTagName( $ret['newgroups'], 'g' );
462 }
463 if ( isset( $ret['oldmetadata'] ) ) {
464 ApiResult::setArrayType( $ret['oldmetadata'], 'array' );
465 ApiResult::setIndexedTagName( $ret['oldmetadata'], 'g' );
466 }
467 if ( isset( $ret['newmetadata'] ) ) {
468 ApiResult::setArrayType( $ret['newmetadata'], 'array' );
469 ApiResult::setIndexedTagName( $ret['newmetadata'], 'g' );
470 }
471 return $ret;
472 }
473
477 private function makeGroupArray( $group ): array {
478 // Migrate old group params from string to array
479 // Handle weird entries caused by a bug in the BackfillInterwikiRightsLog script (T430723)
480 if ( $group === '' || $group === [ '' ] ) {
481 $group = [];
482 } elseif ( is_string( $group ) ) {
483 $group = array_map( 'trim', explode( ',', $group ) );
484 }
485 return $group;
486 }
487}
488
490class_alias( RightsLogFormatter::class, 'RightsLogFormatter' );
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:71
This class represents the result of the API operations.
Definition ApiResult.php:34
Implements the default log formatting.
canView( $field)
Check if a log item can be displayed.
msg( $key,... $params)
Shortcut for wfMessage which honors local context.
This class formats rights log entries.
classifyGroupChanges(array $oldGroups, array $newGroups)
Compares the user groups from before and after this log entry and splits them into four categories: g...
getNewGroups(array $params)
Returns the new groups related to this log entry together with their expiry times.
formatParametersForApi()
Format parameters for API output.The result array should generally map named keys to values....
getParametersForApi()
Get the array of parameters, converted from legacy format if necessary.1.25 to override array
formatChangesToGroups(array $groupChanges, string $performerName, string $targetName, string $messagePrefix='logentry-rights-rights-')
Wraps the changes to user groups into a human-readable messages, so that they can be passed as a para...
getCommentWikiId()
Returns the wiki ID to use for resolving links in the log comment.When a log entry has been replicate...
getMessageKey()
Returns a key to be used for formatting the action sentence.Default is logentry-TYPE-SUBTYPE for mode...
getOldGroups(array $params)
Returns the old groups related to this log entry together with their expiry times.
getMessageParameters()
Formats parameters intended for action message from array of all parameters.There are three hardcoded...
makePageLink(?Title $title=null, $parameters=[], $html=null)
Helper to make a link to the page, taking the plaintext value in consideration.to override string wik...
replaceGroupsWithMemberNames(array &$groupNames)
Replaces the group names in the array with their localized member names.
joinGroupsWithExpiries( $groupNames, array $metadata)
Joins group names from one array with their expiry times from the another.
shouldProcessParams(array $params)
Checks whether the additional message parameters should be processed.
A class containing constants representing the names of configuration variables.
const UserrightsInterwikiDelimiter
Name constant for the UserrightsInterwikiDelimiter setting, for use with Config::get()
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:144
Represents a title within MediaWiki.
Definition Title.php:69
Class to parse and build external user names.
Tools for dealing with other locally-hosted wikis.
Definition WikiMap.php:19
msg( $key,... $params)