MediaWiki  1.30.0
rebuildrecentchanges.php
Go to the documentation of this file.
1 <?php
26 require_once __DIR__ . '/Maintenance.php';
28 
36  private $cutoffFrom;
38  private $cutoffTo;
39 
40  public function __construct() {
41  parent::__construct();
42  $this->addDescription( 'Rebuild recent changes' );
43 
44  $this->addOption(
45  'from',
46  "Only rebuild rows in requested time range (in YYYYMMDDHHMMSS format)",
47  false,
48  true
49  );
50  $this->addOption(
51  'to',
52  "Only rebuild rows in requested time range (in YYYYMMDDHHMMSS format)",
53  false,
54  true
55  );
56  $this->setBatchSize( 200 );
57  }
58 
59  public function execute() {
60  if (
61  ( $this->hasOption( 'from' ) && !$this->hasOption( 'to' ) ) ||
62  ( !$this->hasOption( 'from' ) && $this->hasOption( 'to' ) )
63  ) {
64  $this->error( "Both 'from' and 'to' must be given, or neither", 1 );
65  }
66 
72  if ( !( $this->hasOption( 'from' ) && $this->hasOption( 'to' ) ) ) {
73  $this->purgeFeeds();
74  }
75  $this->output( "Done.\n" );
76  }
77 
81  private function rebuildRecentChangesTablePass1() {
82  $dbw = $this->getDB( DB_MASTER );
83  $revCommentStore = new CommentStore( 'rev_comment' );
84  $rcCommentStore = new CommentStore( 'rc_comment' );
85 
86  if ( $this->hasOption( 'from' ) && $this->hasOption( 'to' ) ) {
87  $this->cutoffFrom = wfTimestamp( TS_UNIX, $this->getOption( 'from' ) );
88  $this->cutoffTo = wfTimestamp( TS_UNIX, $this->getOption( 'to' ) );
89 
90  $sec = $this->cutoffTo - $this->cutoffFrom;
91  $days = $sec / 24 / 3600;
92  $this->output( "Rebuilding range of $sec seconds ($days days)\n" );
93  } else {
95 
96  $days = $wgRCMaxAge / 24 / 3600;
97  $this->output( "Rebuilding \$wgRCMaxAge=$wgRCMaxAge seconds ($days days)\n" );
98 
99  $this->cutoffFrom = time() - $wgRCMaxAge;
100  $this->cutoffTo = time();
101  }
102 
103  $this->output( "Clearing recentchanges table for time range...\n" );
104  $rcids = $dbw->selectFieldValues(
105  'recentchanges',
106  'rc_id',
107  [
108  'rc_timestamp > ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffFrom ) ),
109  'rc_timestamp < ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffTo ) )
110  ]
111  );
112  foreach ( array_chunk( $rcids, $this->mBatchSize ) as $rcidBatch ) {
113  $dbw->delete( 'recentchanges', [ 'rc_id' => $rcidBatch ], __METHOD__ );
114  wfGetLBFactory()->waitForReplication();
115  }
116 
117  $this->output( "Loading from page and revision tables...\n" );
118 
119  $commentQuery = $revCommentStore->getJoin();
120  $res = $dbw->select(
121  [ 'revision', 'page' ] + $commentQuery['tables'],
122  [
123  'rev_timestamp',
124  'rev_user',
125  'rev_user_text',
126  'rev_minor_edit',
127  'rev_id',
128  'rev_deleted',
129  'page_namespace',
130  'page_title',
131  'page_is_new',
132  'page_id'
133  ] + $commentQuery['fields'],
134  [
135  'rev_timestamp > ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffFrom ) ),
136  'rev_timestamp < ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffTo ) )
137  ],
138  __METHOD__,
139  [ 'ORDER BY' => 'rev_timestamp DESC' ],
140  [
141  'page' => [ 'JOIN', 'rev_page=page_id' ],
142  ] + $commentQuery['joins']
143  );
144 
145  $this->output( "Inserting from page and revision tables...\n" );
146  $inserted = 0;
147  foreach ( $res as $row ) {
148  $comment = $revCommentStore->getComment( $row );
149  $dbw->insert(
150  'recentchanges',
151  [
152  'rc_timestamp' => $row->rev_timestamp,
153  'rc_user' => $row->rev_user,
154  'rc_user_text' => $row->rev_user_text,
155  'rc_namespace' => $row->page_namespace,
156  'rc_title' => $row->page_title,
157  'rc_minor' => $row->rev_minor_edit,
158  'rc_bot' => 0,
159  'rc_new' => $row->page_is_new,
160  'rc_cur_id' => $row->page_id,
161  'rc_this_oldid' => $row->rev_id,
162  'rc_last_oldid' => 0, // is this ok?
163  'rc_type' => $row->page_is_new ? RC_NEW : RC_EDIT,
164  'rc_source' => $row->page_is_new ? RecentChange::SRC_NEW : RecentChange::SRC_EDIT,
165  'rc_deleted' => $row->rev_deleted
166  ] + $rcCommentStore->insert( $dbw, $comment ),
167  __METHOD__
168  );
169  if ( ( ++$inserted % $this->mBatchSize ) == 0 ) {
170  wfGetLBFactory()->waitForReplication();
171  }
172  }
173  }
174 
179  private function rebuildRecentChangesTablePass2() {
180  $dbw = $this->getDB( DB_MASTER );
181 
182  $this->output( "Updating links and size differences...\n" );
183 
184  # Fill in the rc_last_oldid field, which points to the previous edit
185  $res = $dbw->select(
186  'recentchanges',
187  [ 'rc_cur_id', 'rc_this_oldid', 'rc_timestamp' ],
188  [
189  "rc_timestamp > " . $dbw->addQuotes( $dbw->timestamp( $this->cutoffFrom ) ),
190  "rc_timestamp < " . $dbw->addQuotes( $dbw->timestamp( $this->cutoffTo ) )
191  ],
192  __METHOD__,
193  [ 'ORDER BY' => 'rc_cur_id,rc_timestamp' ]
194  );
195 
196  $lastCurId = 0;
197  $lastOldId = 0;
198  $lastSize = null;
199  $updated = 0;
200  foreach ( $res as $obj ) {
201  $new = 0;
202 
203  if ( $obj->rc_cur_id != $lastCurId ) {
204  # Switch! Look up the previous last edit, if any
205  $lastCurId = intval( $obj->rc_cur_id );
206  $emit = $obj->rc_timestamp;
207 
208  $row = $dbw->selectRow(
209  'revision',
210  [ 'rev_id', 'rev_len' ],
211  [ 'rev_page' => $lastCurId, "rev_timestamp < " . $dbw->addQuotes( $emit ) ],
212  __METHOD__,
213  [ 'ORDER BY' => 'rev_timestamp DESC' ]
214  );
215  if ( $row ) {
216  $lastOldId = intval( $row->rev_id );
217  # Grab the last text size if available
218  $lastSize = !is_null( $row->rev_len ) ? intval( $row->rev_len ) : null;
219  } else {
220  # No previous edit
221  $lastOldId = 0;
222  $lastSize = null;
223  $new = 1; // probably true
224  }
225  }
226 
227  if ( $lastCurId == 0 ) {
228  $this->output( "Uhhh, something wrong? No curid\n" );
229  } else {
230  # Grab the entry's text size
231  $size = (int)$dbw->selectField(
232  'revision',
233  'rev_len',
234  [ 'rev_id' => $obj->rc_this_oldid ],
235  __METHOD__
236  );
237 
238  $dbw->update(
239  'recentchanges',
240  [
241  'rc_last_oldid' => $lastOldId,
242  'rc_new' => $new,
243  'rc_type' => $new ? RC_NEW : RC_EDIT,
244  'rc_source' => $new === 1 ? RecentChange::SRC_NEW : RecentChange::SRC_EDIT,
245  'rc_old_len' => $lastSize,
246  'rc_new_len' => $size,
247  ],
248  [
249  'rc_cur_id' => $lastCurId,
250  'rc_this_oldid' => $obj->rc_this_oldid,
251  'rc_timestamp' => $obj->rc_timestamp // index usage
252  ],
253  __METHOD__
254  );
255 
256  $lastOldId = intval( $obj->rc_this_oldid );
257  $lastSize = $size;
258 
259  if ( ( ++$updated % $this->mBatchSize ) == 0 ) {
260  wfGetLBFactory()->waitForReplication();
261  }
262  }
263  }
264  }
265 
269  private function rebuildRecentChangesTablePass3() {
271 
272  $dbw = $this->getDB( DB_MASTER );
273  $logCommentStore = new CommentStore( 'log_comment' );
274  $rcCommentStore = new CommentStore( 'rc_comment' );
275 
276  $this->output( "Loading from user, page, and logging tables...\n" );
277 
278  $commentQuery = $logCommentStore->getJoin();
279  $res = $dbw->select(
280  [ 'user', 'logging', 'page' ] + $commentQuery['tables'],
281  [
282  'log_timestamp',
283  'log_user',
284  'user_name',
285  'log_namespace',
286  'log_title',
287  'page_id',
288  'log_type',
289  'log_action',
290  'log_id',
291  'log_params',
292  'log_deleted'
293  ] + $commentQuery['fields'],
294  [
295  'log_timestamp > ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffFrom ) ),
296  'log_timestamp < ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffTo ) ),
297  'log_user=user_id',
298  // Some logs don't go in RC since they are private.
299  // @FIXME: core/extensions also have spammy logs that don't go in RC.
300  'log_type' => array_diff( $wgLogTypes, array_keys( $wgLogRestrictions ) ),
301  ],
302  __METHOD__,
303  [ 'ORDER BY' => 'log_timestamp DESC' ],
304  [
305  'page' =>
306  [ 'LEFT JOIN', [ 'log_namespace=page_namespace', 'log_title=page_title' ] ]
307  ] + $commentQuery['joins']
308  );
309 
310  $field = $dbw->fieldInfo( 'recentchanges', 'rc_cur_id' );
311 
312  $inserted = 0;
313  foreach ( $res as $row ) {
314  $comment = $logCommentStore->getComment( $row );
315  $dbw->insert(
316  'recentchanges',
317  [
318  'rc_timestamp' => $row->log_timestamp,
319  'rc_user' => $row->log_user,
320  'rc_user_text' => $row->user_name,
321  'rc_namespace' => $row->log_namespace,
322  'rc_title' => $row->log_title,
323  'rc_minor' => 0,
324  'rc_bot' => 0,
325  'rc_patrolled' => 1,
326  'rc_new' => 0,
327  'rc_this_oldid' => 0,
328  'rc_last_oldid' => 0,
329  'rc_type' => RC_LOG,
330  'rc_source' => RecentChange::SRC_LOG,
331  'rc_cur_id' => $field->isNullable()
332  ? $row->page_id
333  : (int)$row->page_id, // NULL => 0,
334  'rc_log_type' => $row->log_type,
335  'rc_log_action' => $row->log_action,
336  'rc_logid' => $row->log_id,
337  'rc_params' => $row->log_params,
338  'rc_deleted' => $row->log_deleted
339  ] + $rcCommentStore->insert( $dbw, $comment ),
340  __METHOD__
341  );
342 
343  if ( ( ++$inserted % $this->mBatchSize ) == 0 ) {
344  wfGetLBFactory()->waitForReplication();
345  }
346  }
347  }
348 
352  private function rebuildRecentChangesTablePass4() {
354 
355  $dbw = $this->getDB( DB_MASTER );
356 
357  list( $recentchanges, $usergroups, $user ) =
358  $dbw->tableNamesN( 'recentchanges', 'user_groups', 'user' );
359 
360  # @FIXME: recognize other bot account groups (not the same as users with 'bot' rights)
361  # @NOTE: users with 'bot' rights choose when edits are bot edits or not. That information
362  # may be lost at this point (aside from joining on the patrol log table entries).
363  $botgroups = [ 'bot' ];
364  $autopatrolgroups = $wgUseRCPatrol ? User::getGroupsWithPermission( 'autopatrol' ) : [];
365 
366  # Flag our recent bot edits
367  if ( $botgroups ) {
368  $botwhere = $dbw->makeList( $botgroups );
369 
370  $this->output( "Flagging bot account edits...\n" );
371 
372  # Find all users that are bots
373  $sql = "SELECT DISTINCT user_name FROM $usergroups, $user " .
374  "WHERE ug_group IN($botwhere) AND user_id = ug_user";
375  $res = $dbw->query( $sql, __METHOD__ );
376 
377  $botusers = [];
378  foreach ( $res as $obj ) {
379  $botusers[] = $obj->user_name;
380  }
381 
382  # Fill in the rc_bot field
383  if ( $botusers ) {
384  $rcids = $dbw->selectFieldValues(
385  'recentchanges',
386  'rc_id',
387  [
388  'rc_user_text' => $botusers,
389  "rc_timestamp > " . $dbw->addQuotes( $dbw->timestamp( $this->cutoffFrom ) ),
390  "rc_timestamp < " . $dbw->addQuotes( $dbw->timestamp( $this->cutoffTo ) )
391  ],
392  __METHOD__
393  );
394 
395  foreach ( array_chunk( $rcids, $this->mBatchSize ) as $rcidBatch ) {
396  $dbw->update(
397  'recentchanges',
398  [ 'rc_bot' => 1 ],
399  [ 'rc_id' => $rcidBatch ],
400  __METHOD__
401  );
402  wfGetLBFactory()->waitForReplication();
403  }
404  }
405  }
406 
407  # Flag our recent autopatrolled edits
408  if ( !$wgMiserMode && $autopatrolgroups ) {
409  $patrolwhere = $dbw->makeList( $autopatrolgroups );
410  $patrolusers = [];
411 
412  $this->output( "Flagging auto-patrolled edits...\n" );
413 
414  # Find all users in RC with autopatrol rights
415  $sql = "SELECT DISTINCT user_name FROM $usergroups, $user " .
416  "WHERE ug_group IN($patrolwhere) AND user_id = ug_user";
417  $res = $dbw->query( $sql, __METHOD__ );
418 
419  foreach ( $res as $obj ) {
420  $patrolusers[] = $dbw->addQuotes( $obj->user_name );
421  }
422 
423  # Fill in the rc_patrolled field
424  if ( $patrolusers ) {
425  $patrolwhere = implode( ',', $patrolusers );
426  $sql2 = "UPDATE $recentchanges SET rc_patrolled=1 " .
427  "WHERE rc_user_text IN($patrolwhere) " .
428  "AND rc_timestamp > " . $dbw->addQuotes( $dbw->timestamp( $this->cutoffFrom ) ) . ' ' .
429  "AND rc_timestamp < " . $dbw->addQuotes( $dbw->timestamp( $this->cutoffTo ) );
430  $dbw->query( $sql2 );
431  }
432  }
433  }
434 
439  private function rebuildRecentChangesTablePass5() {
440  $dbw = wfGetDB( DB_MASTER );
441 
442  $this->output( "Removing duplicate revision and logging entries...\n" );
443 
444  $res = $dbw->select(
445  [ 'logging', 'log_search' ],
446  [ 'ls_value', 'ls_log_id' ],
447  [
448  'ls_log_id = log_id',
449  'ls_field' => 'associated_rev_id',
450  'log_type' => 'upload',
451  'log_timestamp > ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffFrom ) ),
452  'log_timestamp < ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffTo ) ),
453  ],
454  __METHOD__
455  );
456 
457  $updates = 0;
458  foreach ( $res as $obj ) {
459  $rev_id = $obj->ls_value;
460  $log_id = $obj->ls_log_id;
461 
462  // Mark the logging row as having an associated rev id
463  $dbw->update(
464  'recentchanges',
465  /*SET*/ [ 'rc_this_oldid' => $rev_id ],
466  /*WHERE*/ [ 'rc_logid' => $log_id ],
467  __METHOD__
468  );
469 
470  // Delete the revision row
471  $dbw->delete(
472  'recentchanges',
473  /*WHERE*/ [ 'rc_this_oldid' => $rev_id, 'rc_logid' => 0 ],
474  __METHOD__
475  );
476 
477  if ( ( ++$updates % $this->mBatchSize ) == 0 ) {
478  wfGetLBFactory()->waitForReplication();
479  }
480  }
481  }
482 
486  private function purgeFeeds() {
488 
489  $this->output( "Deleting feed timestamps.\n" );
490 
491  $wanCache = MediaWikiServices::getInstance()->getMainWANObjectCache();
492  foreach ( $wgFeedClasses as $feed => $className ) {
493  $wanCache->delete( $wanCache->makeKey( 'rcfeed', $feed, 'timestamp' ) ); # Good enough for now.
494  }
495  }
496 }
497 
498 $maintClass = "RebuildRecentchanges";
499 require_once RUN_MAINTENANCE_IF_MAIN;
RebuildRecentchanges\$cutoffTo
int $cutoffTo
UNIX timestamp.
Definition: rebuildrecentchanges.php:38
$user
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a account $user
Definition: hooks.txt:244
Maintenance\addDescription
addDescription( $text)
Set the description text.
Definition: Maintenance.php:287
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:2040
RC_LOG
const RC_LOG
Definition: Defines.php:145
use
as see the revision history and available at free of to any person obtaining a copy of this software and associated documentation to deal in the Software without including without limitation the rights to use
Definition: MIT-LICENSE.txt:10
CommentStore
CommentStore handles storage of comments (edit summaries, log reasons, etc) in the database.
Definition: CommentStore.php:30
RUN_MAINTENANCE_IF_MAIN
require_once RUN_MAINTENANCE_IF_MAIN
Definition: maintenance.txt:50
RC_EDIT
const RC_EDIT
Definition: Defines.php:143
RebuildRecentchanges\execute
execute()
Do the actual work.
Definition: rebuildrecentchanges.php:59
$res
$res
Definition: database.txt:21
Maintenance
Abstract maintenance class for quickly writing and churning out maintenance scripts with minimal effo...
Definition: maintenance.txt:39
RebuildRecentchanges\rebuildRecentChangesTablePass5
rebuildRecentChangesTablePass5()
Rebuild pass 5: Delete duplicate entries where we generate both a page revision and a log entry for a...
Definition: rebuildrecentchanges.php:439
$wgUseRCPatrol
$wgUseRCPatrol
Use RC Patrolling to check for vandalism (from recent changes and watchlists) New pages and new files...
Definition: DefaultSettings.php:6801
RecentChange\SRC_LOG
const SRC_LOG
Definition: RecentChange.php:72
php
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2856
$maintClass
$maintClass
Definition: rebuildrecentchanges.php:498
Maintenance\addOption
addOption( $name, $description, $required=false, $withArg=false, $shortName=false, $multiOccurrence=false)
Add a parameter to the script.
Definition: Maintenance.php:215
$wgLogTypes
$wgLogTypes
The logging system has two levels: an event type, which describes the general category and can be vie...
Definition: DefaultSettings.php:7568
RecentChange\SRC_EDIT
const SRC_EDIT
Definition: RecentChange.php:70
global
when a variable name is used in a it is silently declared as a new masking the global
Definition: design.txt:93
DB_MASTER
const DB_MASTER
Definition: defines.php:26
RecentChange\SRC_NEW
const SRC_NEW
Definition: RecentChange.php:71
$wgFeedClasses
$wgFeedClasses
Available feeds objects.
Definition: DefaultSettings.php:6893
RebuildRecentchanges
Maintenance script that rebuilds recent changes from scratch.
Definition: rebuildrecentchanges.php:34
list
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition: deferred.txt:11
$wgLogRestrictions
$wgLogRestrictions
This restricts log access to those who have a certain right Users without this will not see it in the...
Definition: DefaultSettings.php:7592
$wgRCMaxAge
$wgRCMaxAge
Recentchanges items are periodically purged; entries older than this many seconds will go.
Definition: DefaultSettings.php:6681
RebuildRecentchanges\rebuildRecentChangesTablePass1
rebuildRecentChangesTablePass1()
Rebuild pass 1: Insert recentchanges entries for page revisions.
Definition: rebuildrecentchanges.php:81
RebuildRecentchanges\rebuildRecentChangesTablePass4
rebuildRecentChangesTablePass4()
Rebuild pass 4: Mark bot and autopatrolled entries.
Definition: rebuildrecentchanges.php:352
RebuildRecentchanges\rebuildRecentChangesTablePass3
rebuildRecentChangesTablePass3()
Rebuild pass 3: Insert recentchanges entries for action logs.
Definition: rebuildrecentchanges.php:269
RC_NEW
const RC_NEW
Definition: Defines.php:144
wfGetLBFactory
wfGetLBFactory()
Get the load balancer factory object.
Definition: GlobalFunctions.php:2885
$wgMiserMode
$wgMiserMode
Disable database-intensive features.
Definition: DefaultSettings.php:2166
Maintenance\getOption
getOption( $name, $default=null)
Get an option, or return the default.
Definition: Maintenance.php:250
RebuildRecentchanges\$cutoffFrom
int $cutoffFrom
UNIX timestamp.
Definition: rebuildrecentchanges.php:36
as
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
Maintenance\getDB
getDB( $db, $groups=[], $wiki=false)
Returns a database to be used by current maintenance script.
Definition: Maintenance.php:1251
Maintenance\error
error( $err, $die=0)
Throw an error to the user.
Definition: Maintenance.php:392
Maintenance\output
output( $out, $channel=null)
Throw some output to the user.
Definition: Maintenance.php:373
MediaWikiServices
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency MediaWikiServices
Definition: injection.txt:23
Maintenance\hasOption
hasOption( $name)
Checks to see if a particular param exists.
Definition: Maintenance.php:236
RebuildRecentchanges\__construct
__construct()
Default constructor.
Definition: rebuildrecentchanges.php:40
RebuildRecentchanges\purgeFeeds
purgeFeeds()
Purge cached feeds in $wanCache.
Definition: rebuildrecentchanges.php:486
RebuildRecentchanges\rebuildRecentChangesTablePass2
rebuildRecentChangesTablePass2()
Rebuild pass 2: Enhance entries for page revisions with references to the previous revision (rc_last_...
Definition: rebuildrecentchanges.php:179
Maintenance\setBatchSize
setBatchSize( $s=0)
Set the batch size.
Definition: Maintenance.php:314
User\getGroupsWithPermission
static getGroupsWithPermission( $role)
Get all the groups who have a given permission.
Definition: User.php:4768