Passed
Push — master ( 74dea7...29a7ff )
by Daimona
02:00
created

UpdateList::getExtraAdminGroups()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 8
nc 4
nop 1
dl 0
loc 11
rs 10
c 0
b 0
f 0
1
<?php declare( strict_types=1 );
2
3
namespace BotRiconferme\Task;
4
5
use BotRiconferme\TaskHelper\TaskResult;
6
use BotRiconferme\Wiki\Page\PageBotList;
7
use Generator;
8
9
/**
10
 * Updates the JSON list, adding and removing dates according to the API list of privileged people
11
 */
12
class UpdateList extends Task {
13
	private const RELEVANT_GROUPS = [ 'sysop', 'bureaucrat', 'checkuser' ];
14
	private const NON_GROUP_KEYS = [ 'override' => 1, 'override-perm' => 1, 'aliases' => 1 ];
15
16
	/**
17
	 * @var array[] The list from the API request, mapping [ user => group[] ]
18
	 * @phan-var array<string,list<string>>
19
	 */
20
	private $actualList;
21
22
	/**
23
	 * @inheritDoc
24
	 */
25
	protected function getSubtasksMap(): array {
26
		// Everything is done here.
27
		return [];
28
	}
29
30
	/**
31
	 * @inheritDoc
32
	 */
33
	public function runInternal(): int {
34
		$this->actualList = $this->computeActualList();
35
		$pageBotList = $this->getBotList();
36
		$botList = $pageBotList->getDecodedContent();
37
38
		$newList = $this->computeNewList( $botList );
39
40
		if ( $newList === $botList ) {
41
			return TaskResult::STATUS_NOTHING;
42
		}
43
44
		$this->getLogger()->info( 'Updating admin list' );
45
46
		$pageBotList->edit( [
47
			'text' => json_encode( $newList ),
48
			'summary' => $this->msg( 'list-update-summary' )->text()
49
		] );
50
51
		return $this->errors ? TaskResult::STATUS_ERROR : TaskResult::STATUS_GOOD;
52
	}
53
54
	/**
55
	 * @return string[][]
56
	 * @phan-return array<string,string[]>
57
	 */
58
	private function computeActualList(): array {
59
		$params = [
60
			'action' => 'query',
61
			'list' => 'allusers',
62
			'augroup' => 'sysop',
63
			'auprop' => 'groups',
64
			'aulimit' => 'max',
65
		];
66
67
		$req = $this->getWiki()->getRequestFactory()->createStandaloneRequest( $params );
68
		return $this->extractAdminsData( $req->executeAsQuery() );
69
	}
70
71
	/**
72
	 * @param Generator $data
73
	 * @return string[][]
74
	 * @phan-return array<string,string[]>
75
	 */
76
	private function extractAdminsData( Generator $data ): array {
77
		$ret = [];
78
		$blacklist = $this->getOpt( 'exclude-admins' );
79
		foreach ( $data as $user ) {
80
			if ( in_array( $user->name, $blacklist, true ) ) {
81
				continue;
82
			}
83
			$interestingGroups = array_intersect( $user->groups, self::RELEVANT_GROUPS );
84
			$ret[ $user->name ] = array_values( $interestingGroups );
85
		}
86
		return $ret;
87
	}
88
89
	/**
90
	 * Get the new content for the list
91
	 *
92
	 * @param array[] $curList
93
	 * @phan-param array<string,array{sysop:string,checkuser?:string,bureaucrat?:string,override?:string,override-perm?:string,aliases?:list<string>}> $curList
94
	 * @return array[]
95
	 */
96
	private function computeNewList( array $curList ): array {
97
		$newList = $curList;
98
99
		$extra = $this->getExtraAdminGroups( $curList );
100
		if ( $extra ) {
101
			$renamed = $this->handleRenames( $newList, $extra );
102
			$extra = array_diff_key( $extra, $renamed );
103
		}
104
		$this->handleExtraAndMissing( $newList, $extra );
105
		$this->removeOverrides( $newList );
106
107
		ksort( $newList, SORT_STRING | SORT_FLAG_CASE );
108
		return $newList;
109
	}
110
111
112
	/**
113
	 * @param array &$newList
114
	 * @phan-param array<string,array<string,string|string[]>> $newContent
115
	 * @param array $extra
116
	 */
117
	private function handleExtraAndMissing( array &$newList, array $extra ): void {
118
		$missing = $this->getMissingAdminGroups( $newList );
119
120
		$removed = [];
121
		foreach ( $newList as $user => $data ) {
122
			if ( isset( $missing[$user] ) ) {
123
				$newList[$user] = array_merge( $data, $missing[$user] );
124
				unset( $missing[$user] );
125
			} elseif ( isset( $extra[$user] ) ) {
126
				$newGroups = array_diff_key( $data, $extra[$user] );
127
				if ( array_diff_key( $newGroups, self::NON_GROUP_KEYS ) ) {
128
					$newList[$user] = $newGroups;
129
				} else {
130
					$removed[] = $user;
131
					unset( $newList[$user] );
132
				}
133
			}
134
		}
135
		// Add users which don't have an entry at all
136
		$newList = array_merge( $newList, $missing );
137
		$this->getLogger()->info( 'The following admins were removed: ' . implode( ', ', $removed ) );
138
	}
139
140
	/**
141
	 * Populate a list of new admins missing from the JSON list and their groups
142
	 *
143
	 * @param array $botList
144
	 * @return string[][]
145
	 */
146
	private function getMissingAdminGroups( array $botList ): array {
147
		$missing = [];
148
		foreach ( $this->actualList as $admin => $groups ) {
149
			$missingGroups = array_diff( $groups, array_keys( $botList[$admin] ?? [] ) );
150
			foreach ( $missingGroups as $group ) {
151
				$ts = $this->getFlagDate( $admin, $group );
152
				if ( $ts === null ) {
153
					$this->errors[] = "$group flag date unavailable for $admin";
154
					continue;
155
				}
156
				$missing[$admin][$group] = $ts;
157
			}
158
		}
159
		return $missing;
160
	}
161
162
	/**
163
	 * Get the flag date for the given admin and group.
164
	 *
165
	 * @param string $admin
166
	 * @param string $group
167
	 * @return string|null
168
	 */
169
	private function getFlagDate( string $admin, string $group ): ?string {
170
		$this->getLogger()->info( "Retrieving $group flag date for $admin" );
171
172
		$wiki = $this->getWiki();
173
		if ( $group === 'checkuser' ) {
174
			$wiki = $this->getWikiGroup()->getCentralWiki();
175
			$admin .= $wiki->getLocalUserIdentifier();
176
		}
177
178
		$params = [
179
			'action' => 'query',
180
			'list' => 'logevents',
181
			'leprop' => 'timestamp|details',
182
			'leaction' => 'rights/rights',
183
			'letitle' => "User:$admin",
184
			'lelimit' => 'max'
185
		];
186
187
		$data = $wiki->getRequestFactory()->createStandaloneRequest( $params )->executeAsQuery();
188
		$ts = $this->extractTimestamp( $data, $group );
189
190
		return $ts !== null ? date( 'd/m/Y', strtotime( $ts ) ) : null;
191
	}
192
193
	/**
194
	 * Find the actual timestamp when the user was given the searched group
195
	 *
196
	 * @param Generator $data
197
	 * @param string $group
198
	 * @return string|null
199
	 */
200
	private function extractTimestamp( Generator $data, string $group ): ?string {
201
		$ts = null;
202
		foreach ( $data as $entry ) {
203
			if (
204
				isset( $entry->params ) &&
205
				in_array( $group, array_diff( $entry->params->newgroups, $entry->params->oldgroups ), true )
206
			) {
207
				$ts = $entry->timestamp;
208
				break;
209
			}
210
		}
211
		return $ts;
212
	}
213
214
	/**
215
	 * Get a list of admins who are in the JSON page but don't have the listed privileges anymore
216
	 *
217
	 * @param array[] $botList
218
	 * @phan-param array<string,array{sysop:string,checkuser?:string,bureaucrat?:string,override?:string,override-perm?:string,aliases?:list<string>}> $botList
219
	 * @return string[][]
220
	 */
221
	private function getExtraAdminGroups( array $botList ): array {
222
		$extra = [];
223
		foreach ( $botList as $name => $data ) {
224
			$groups = array_diff_key( $data, self::NON_GROUP_KEYS );
225
			if ( !isset( $this->actualList[$name] ) ) {
226
				$extra[$name] = $groups;
227
			} elseif ( count( $groups ) > count( $this->actualList[$name] ) ) {
228
				$extra[$name] = array_diff_key( $groups, $this->actualList[$name] );
229
			}
230
		}
231
		return $extra;
232
	}
233
234
	/**
235
	 * @param string[] $oldNames
236
	 * @return Generator
237
	 */
238
	private function getRenameEntries( array $oldNames ): Generator {
239
		$titles = array_map( static function ( string $x ): string {
240
			return "Utente:$x";
241
		}, $oldNames );
242
243
		$params = [
244
			'action' => 'query',
245
			'list' => 'logevents',
246
			'leprop' => 'title|details|timestamp',
247
			'letype' => 'renameuser',
248
			'letitle' => implode( '|', $titles ),
249
			'lelimit' => 'max',
250
			// lestart seems to be broken (?)
251
		];
252
253
		return $this->getWiki()->getRequestFactory()->createStandaloneRequest( $params )->executeAsQuery();
254
	}
255
256
	/**
257
	 * Given a list of (old) usernames, check if these people have been renamed recently.
258
	 *
259
	 * @param string[] $oldNames
260
	 * @return string[] [ old_name => new_name ]
261
	 */
262
	private function getRenamedUsers( array $oldNames ): array {
263
		if ( !$oldNames ) {
264
			return [];
265
		}
266
		$this->getLogger()->info( 'Checking rename for ' . implode( ', ', $oldNames ) );
267
268
		$data = $this->getRenameEntries( $oldNames );
269
		$ret = [];
270
		foreach ( $data as $entry ) {
271
			// 1 month is arbitrary
272
			if ( strtotime( $entry->timestamp ) > strtotime( '-1 month' ) ) {
273
				$par = $entry->params;
274
				$ret[ $par->olduser ] = $par->newuser;
275
			}
276
		}
277
		$this->getLogger()->info( 'Renames found: ' . var_export( $ret, true ) );
278
		return $ret;
279
	}
280
281
	/**
282
	 * Checks whether any user that is on the bot list but is not an admin according to MW
283
	 * was actually renamed, and updates the list accordingly.
284
	 *
285
	 * @param array &$newList
286
	 * @phan-param array<string,array<string,string|string[]>> $newContent
287
	 * @param string[][] $extra
288
	 * @return array<string,string> Map of renamed users
289
	 */
290
	private function handleRenames( array &$newList, array $extra ): array {
291
		$renameMap = $this->getRenamedUsers( array_keys( $extra ) );
292
		foreach ( $renameMap as $oldName => $newName ) {
293
			$this->getLogger()->info( "Found rename $oldName -> $newName" );
294
			$newList[$newName] = $newList[$oldName];
295
			$newList[$newName]['aliases'] = array_unique( array_merge( $newList[$newName]['aliases'] ?? [], [ $oldName ] ) );
296
			unset( $newList[$oldName] );
297
		}
298
		return $renameMap;
299
	}
300
301
	/**
302
	 * Remove expired overrides.
303
	 *
304
	 * @param array[] &$newList
305
	 */
306
	private function removeOverrides( array &$newList ): void {
307
		$removed = [];
308
		foreach ( $newList as $user => $data ) {
309
			if ( PageBotList::isOverrideExpired( $data ) ) {
310
				unset( $newList[$user]['override'] );
311
				$removed[] = $user;
312
			}
313
		}
314
315
		if ( $removed ) {
316
			$this->getLogger()->info( 'Removing overrides for users: ' . implode( ', ', $removed ) );
317
		}
318
	}
319
}
320