Passed
Push — master ( 33abbd...74427d )
by Daimona
02:18
created

UpdateList::getExtraGroups()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 4
eloc 8
nc 4
nop 0
dl 0
loc 11
rs 10
c 2
b 0
f 0
1
<?php declare( strict_types=1 );
2
3
namespace BotRiconferme\Task;
4
5
use BotRiconferme\Wiki\Page\PageBotList;
6
use BotRiconferme\Request\RequestBase;
7
use BotRiconferme\Exception\TaskException;
8
use BotRiconferme\TaskResult;
9
10
/**
11
 * Updates the JSON list, adding and removing dates according to the API list of privileged people
12
 */
13
class UpdateList extends Task {
14
	/** @var array[] The JSON list */
15
	private $botList;
16
	/** @var array[] The list from the API request */
17
	private $actualList;
18
19
	/**
20
	 * @inheritDoc
21
	 */
22
	protected function getSubtasksMap(): array {
23
		// Everything is done here.
24
		return [];
25
	}
26
27
	/**
28
	 * @inheritDoc
29
	 */
30
	public function runInternal() : int {
31
		$this->actualList = $this->getActualAdmins();
32
		$pageBotList = PageBotList::get( $this->getWiki() );
33
		$this->botList = $pageBotList->getAdminsList();
34
35
		$missing = $this->getMissingGroups();
36
		$extra = $this->getExtraGroups();
37
38
		$newContent = $this->getNewContent( $missing, $extra );
39
40
		if ( $newContent === $this->botList ) {
41
			return TaskResult::STATUS_NOTHING;
42
		}
43
44
		$this->getLogger()->info( 'Updating admin list' );
45
46
		$pageBotList->edit( [
47
			'text' => json_encode( $newContent ),
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 array
56
	 */
57
	protected function getActualAdmins() : array {
58
		$params = [
59
			'action' => 'query',
60
			'list' => 'allusers',
61
			'augroup' => 'sysop',
62
			'auprop' => 'groups',
63
			'aulimit' => 'max',
64
		];
65
66
		$req = RequestBase::newFromParams( $params );
67
		return $this->extractAdmins( $req->execute() );
68
	}
69
70
	/**
71
	 * @param \stdClass $data
72
	 * @return array
73
	 */
74
	protected function extractAdmins( \stdClass $data ) : array {
75
		$ret = [];
76
		$blacklist = $this->getOpt( 'exclude-admins' );
77
		foreach ( $data->query->allusers as $u ) {
78
			if ( in_array( $u->name, $blacklist ) ) {
79
				continue;
80
			}
81
			$interestingGroups = array_intersect( $u->groups, [ 'sysop', 'bureaucrat', 'checkuser' ] );
82
			$ret[ $u->name ] = array_values( $interestingGroups );
83
		}
84
		return $ret;
85
	}
86
87
	/**
88
	 * Populate a list of new admins missing from the JSON list and their groups
89
	 *
90
	 * @return array[]
91
	 */
92
	protected function getMissingGroups() : array {
93
		$missing = [];
94
		foreach ( $this->actualList as $adm => $groups ) {
95
			$curMissing = array_diff( $groups, array_keys( $this->botList[$adm] ?? [] ) );
96
97
			foreach ( $curMissing as $group ) {
98
				try {
99
					$missing[ $adm ][ $group ] = $this->getFlagDate( $adm, $group );
100
				} catch ( TaskException $e ) {
101
					$this->errors[] = $e->getMessage();
102
				}
103
			}
104
		}
105
		return $missing;
106
	}
107
108
	/**
109
	 * Get the flag date for the given admin and group.
110
	 *
111
	 * @param string $admin
112
	 * @param string $group
113
	 * @return string
114
	 * @throws TaskException
115
	 */
116
	protected function getFlagDate( string $admin, string $group ) : string {
117
		$this->getLogger()->info( "Retrieving $group flag date for $admin" );
118
119
		$url = DEFAULT_URL;
120
		if ( $group === 'checkuser' ) {
121
			$url = 'https://meta.wikimedia.org/w/api.php';
122
			$admin .= '@itwiki';
123
		}
124
125
		$params = [
126
			'action' => 'query',
127
			'list' => 'logevents',
128
			'leprop' => 'timestamp|details',
129
			'leaction' => 'rights/rights',
130
			'letitle' => "User:$admin",
131
			'lelimit' => 'max'
132
		];
133
134
		// @phan-suppress-next-line PhanTypeMismatchArgumentNullable $url is never null
135
		$data = RequestBase::newFromParams( $params )->setUrl( $url )->execute();
136
		$ts = $this->extractTimestamp( $data, $group );
137
138
		if ( $ts === null ) {
139
			throw new TaskException( "$group flag date unavailable for $admin" );
140
		}
141
142
		return date( 'd/m/Y', strtotime( $ts ) );
143
	}
144
145
	/**
146
	 * Find the actual timestamp when the user was given the searched group
147
	 *
148
	 * @param \stdClass $data
149
	 * @param string $group
150
	 * @return string|null
151
	 */
152
	private function extractTimestamp( \stdClass $data, string $group ) : ?string {
153
		$ts = null;
154
		foreach ( $data->query->logevents as $entry ) {
155
			if ( isset( $entry->params ) ) {
156
				$addedGroups = array_diff( $entry->params->newgroups, $entry->params->oldgroups );
157
				if ( in_array( $group, $addedGroups ) ) {
158
					$ts = $entry->timestamp;
159
					break;
160
				}
161
			}
162
		}
163
		return $ts;
164
	}
165
166
	/**
167
	 * Get a list of admins who are in the JSON page but don't have the listed privileges anymore
168
	 *
169
	 * @return array[]
170
	 */
171
	protected function getExtraGroups() : array {
172
		$extra = [];
173
		foreach ( $this->botList as $name => $groups ) {
174
			$groups = array_diff_key( $groups, PageBotList::NON_GROUP_KEYS );
175
			if ( !isset( $this->actualList[ $name ] ) ) {
176
				$extra[ $name ] = $groups;
177
			} elseif ( count( $groups ) > count( $this->actualList[ $name ] ) ) {
178
				$extra[ $name ] = array_diff_key( $groups, $this->actualList[ $name ] );
179
			}
180
		}
181
		return $extra;
182
	}
183
184
	/**
185
	 * Given a list of (old) usernames, check if these people have been renamed recently.
186
	 *
187
	 * @param string[] $names
188
	 * @return string[] [ new_name => old_name ]
189
	 */
190
	protected function checkRenamedUsers( array $names ) : array {
191
		$titles = array_map( function ( $x ) {
192
			return "Utente:$x";
193
		}, $names );
194
		$params = [
195
			'action' => 'query',
196
			'list' => 'logevents',
197
			'leprop' => 'title|details|timestamp',
198
			'letype' => 'renameuser',
199
			'letitle' => implode( '|', $titles ),
200
			'lelimit' => 'max',
201
			// lestart seems to be broken, so we check below
202
		];
203
204
		$data = RequestBase::newFromParams( $params )->execute();
205
		$ret = [];
206
		foreach ( $data->query->logevents as $entry ) {
207
			$time = strtotime( $entry->timestamp );
208
			// 1 month is arbitrary
209
			if ( $time > strtotime( '-1 month' ) ) {
210
				$par = $entry->params;
211
				$ret[ $par->newuser ] = $par->olduser;
212
			}
213
		}
214
		return $ret;
215
	}
216
217
	/**
218
	 * Get the new content for the list
219
	 *
220
	 * @param array[] $missing
221
	 * @param array[] $extra
222
	 * @return array[]
223
	 */
224
	protected function getNewContent( array $missing, array $extra ) : array {
225
		$newContent = $this->botList;
226
		$renameMap = $this->checkRenamedUsers( array_keys( $extra ) );
227
		$removed = [];
228
		foreach ( $newContent as $user => $groups ) {
229
			if ( isset( $missing[ $user ] ) ) {
230
				$newContent[ $user ] = array_merge( $groups, $missing[ $user ] );
231
				unset( $missing[ $user ] );
232
			} elseif ( isset( $extra[ $user ] ) ) {
233
				$newGroups = array_diff_key( $groups, $extra[ $user ] );
234
				if ( $newGroups ) {
235
					$newContent[ $user ] = $newGroups;
236
				} else {
237
					unset( $newContent[ $user ] );
238
					$removed[] = $user;
239
				}
240
			}
241
		}
242
		// Add users which don't have an entry at all, and remove empty users
243
		// @todo Is the array_filter still necessary?
244
		$newContent = array_filter( array_merge( $newContent, $missing ) );
245
246
		foreach ( $removed as $oldName ) {
247
			if ( array_key_exists( $oldName, $renameMap ) && array_key_exists( $renameMap[$oldName], $newContent ) ) {
248
				$newName = $renameMap[ $oldName ];
249
				// This user was renamed! Add this name as alias... If they're still listed!
250
				if ( array_key_exists( 'aliases', $newContent[ $newName ] ) ) {
251
					if ( !in_array( $oldName, $newContent[ $newName ]['aliases'] ) ) {
252
						$newContent[ $newName ]['aliases'][] = $oldName;
253
					}
254
				} else {
255
					$newContent[ $newName ]['aliases'] = [ $oldName ];
256
				}
257
			}
258
		}
259
260
		$newContent = $this->removeOverrides( $newContent );
261
		ksort( $newContent, SORT_STRING | SORT_FLAG_CASE );
262
		return $newContent;
263
	}
264
265
	/**
266
	 * Remove expired overrides. This must happen after the override date has been used AND
267
	 * after the "normal" date has passed. We do it 3 days later to be sure.
268
	 *
269
	 * @param array[] $newContent
270
	 * @return array[]
271
	 */
272
	protected function removeOverrides( array $newContent ) : array {
273
		$removed = [];
274
		foreach ( $newContent as $user => $groups ) {
275
			if ( !isset( $groups['override'] ) ) {
276
				continue;
277
			}
278
279
			$flagTS = PageBotList::getValidFlagTimestamp( $groups );
280
			$usualTS = strtotime( date( 'Y' ) . '-' . date( 'm-d', $flagTS ) );
281
			$overrideTS = \DateTime::createFromFormat( 'd/m/Y', $groups['override'] )->getTimestamp();
282
			$delay = 60 * 60 * 24 * 3;
283
284
			if ( time() > $usualTS + $delay && time() > $overrideTS + $delay ) {
285
				unset( $newContent[ $user ][ 'override' ] );
286
				$removed[] = $user;
287
			}
288
		}
289
290
		if ( $removed ) {
291
			$this->getLogger()->info( 'Removing overrides for users: ' . implode( ', ', $removed ) );
292
		}
293
294
		return $newContent;
295
	}
296
}
297