Passed
Push — master ( bac275...6454e3 )
by Daimona
02:17
created

UpdateList::removeOverrides()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 19
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 12
nc 6
nop 1
dl 0
loc 19
rs 9.5555
c 0
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
		$this->botList = PageBotList::get()->getAdminsList();
33
34
		$missing = $this->getMissingGroups();
35
		$extra = $this->getExtraGroups();
36
37
		$newContent = $this->botList;
38
		if ( $missing || $extra ) {
39
			$newContent = $this->getNewContent( $missing, $extra );
40
		}
41
42
		if ( $newContent === $this->botList ) {
43
			return TaskResult::STATUS_NOTHING;
44
		}
45
46
		$this->getLogger()->info( 'Updating admin list' );
47
48
		PageBotList::get()->edit( [
49
			'text' => json_encode( $newContent ),
50
			'summary' => $this->msg( 'list-update-summary' )->text()
51
		] );
52
53
		return $this->errors ? TaskResult::STATUS_ERROR : TaskResult::STATUS_GOOD;
54
	}
55
56
	/**
57
	 * @return array
58
	 */
59
	protected function getActualAdmins() : array {
60
		$this->getLogger()->debug( 'Retrieving admins - API' );
61
		$params = [
62
			'action' => 'query',
63
			'list' => 'allusers',
64
			'augroup' => 'sysop',
65
			'auprop' => 'groups',
66
			'aulimit' => 'max',
67
		];
68
69
		$req = RequestBase::newFromParams( $params );
70
		return $this->extractAdmins( $req->execute() );
71
	}
72
73
	/**
74
	 * @param \stdClass $data
75
	 * @return array
76
	 */
77
	protected function extractAdmins( \stdClass $data ) : array {
78
		$ret = [];
79
		$blacklist = $this->getConfig()->get( 'exclude-admins' );
80
		foreach ( $data->query->allusers as $u ) {
81
			if ( in_array( $u->name, $blacklist ) ) {
82
				continue;
83
			}
84
			$interestingGroups = array_intersect( $u->groups, [ 'sysop', 'bureaucrat', 'checkuser' ] );
85
			$ret[ $u->name ] = $interestingGroups;
86
		}
87
		return $ret;
88
	}
89
90
	/**
91
	 * Populate a list of new admins missing from the JSON list and their groups
92
	 *
93
	 * @return array[]
94
	 */
95
	protected function getMissingGroups() : array {
96
		$missing = [];
97
		foreach ( $this->actualList as $adm => $groups ) {
98
			$groupsList = [];
99
			if ( !isset( $this->botList[ $adm ] ) ) {
100
				$groupsList = $groups;
101
			} elseif ( count( $groups ) > count( $this->botList[$adm] ) ) {
102
				// Only some groups are missing
103
				$groupsList = array_diff_key( $groups, $this->botList[$adm] );
104
			}
105
106
			foreach ( $groupsList as $group ) {
107
				try {
108
					$missing[ $adm ][ $group ] = $this->getFlagDate( $adm, $group );
109
				} catch ( TaskException $e ) {
110
					$this->errors[] = $e->getMessage();
111
				}
112
			}
113
		}
114
		return $missing;
115
	}
116
117
	/**
118
	 * Get the flag date for the given admin and group.
119
	 *
120
	 * @param string $admin
121
	 * @param string $group
122
	 * @return string
123
	 * @throws TaskException
124
	 */
125
	protected function getFlagDate( string $admin, string $group ) : string {
126
		$this->getLogger()->info( "Retrieving $group flag date for $admin" );
127
128
		$url = DEFAULT_URL;
129
		if ( $group === 'checkuser' ) {
130
			$url = 'https://meta.wikimedia.org/w/api.php';
131
			$admin .= '@itwiki';
132
		}
133
134
		$params = [
135
			'action' => 'query',
136
			'list' => 'logevents',
137
			'leprop' => 'timestamp|details',
138
			'leaction' => 'rights/rights',
139
			'letitle' => "User:$admin",
140
			'lelimit' => 'max'
141
		];
142
143
		$data = RequestBase::newFromParams( $params )->setUrl( $url )->execute();
144
		$ts = $this->extractTimestamp( $data, $group );
145
146
		if ( $ts === null ) {
147
			throw new TaskException( "$group flag date unavailable for $admin" );
148
		}
149
150
		return date( 'd/m/Y', strtotime( $ts ) );
151
	}
152
153
	/**
154
	 * Find the actual timestamp when the user was given the searched group
155
	 *
156
	 * @param \stdClass $data
157
	 * @param string $group
158
	 * @return string|null
159
	 */
160
	private function extractTimestamp( \stdClass $data, string $group ) : ?string {
161
		$ts = null;
162
		foreach ( $data->query->logevents as $entry ) {
163
			if ( !isset( $entry->params ) ) {
164
				// Old entries
165
				continue;
166
			}
167
168
			$addedGroups = array_diff( $entry->params->newgroups, $entry->params->oldgroups );
169
			if ( in_array( $group, $addedGroups ) ) {
170
				$ts = $entry->timestamp;
171
				break;
172
			}
173
		}
174
		return $ts;
175
	}
176
177
	/**
178
	 * Get a list of admins who are in the JSON page but don't have the listed privileges anymore
179
	 *
180
	 * @return array[]
181
	 */
182
	protected function getExtraGroups() : array {
183
		$extra = [];
184
		foreach ( $this->botList as $name => $groups ) {
185
			// These are not groups
186
			unset( $groups[ 'override' ], $groups[ 'override-perm' ] );
187
			if ( !isset( $this->actualList[ $name ] ) ) {
188
				$extra[ $name ] = $groups;
189
			} elseif ( count( $groups ) > count( $this->actualList[ $name ] ) ) {
190
				$extra[ $name ] = array_diff_key( $groups, $this->actualList[ $name ] );
191
			}
192
		}
193
		return $extra;
194
	}
195
196
	/**
197
	 * Get the new content for the list
198
	 *
199
	 * @param array[] $missing
200
	 * @param array[] $extra
201
	 * @return array[]
202
	 */
203
	protected function getNewContent( array $missing, array $extra ) : array {
204
		$newContent = $this->botList;
205
		foreach ( $newContent as $user => $groups ) {
206
			if ( isset( $missing[ $user ] ) ) {
207
				$newContent[ $user ] = array_merge( $groups, $missing[ $user ] );
208
				unset( $missing[ $user ] );
209
			} elseif ( isset( $extra[ $user ] ) ) {
210
				$newContent[ $user ] = array_diff_key( $groups, $extra[ $user ] );
211
			}
212
		}
213
		// Add users which don't have an entry at all, and remove empty users
214
		$newContent = array_filter( array_merge( $newContent, $missing ) );
215
		$newContent = $this->removeOverrides( $newContent );
216
		ksort( $newContent );
217
		return $newContent;
218
	}
219
220
	/**
221
	 * Remove expired overrides. This must happen after the override date has been used AND
222
	 * after the "normal" date has passed. Given that "override" can only be used to anticipate
223
	 * a date, we remove it the day after the "normal date".
224
	 *
225
	 * @param array[] $newContent
226
	 * @return array[]
227
	 */
228
	protected function removeOverrides( array $newContent ) : array {
229
		$removed = [];
230
		foreach ( $newContent as $user => $groups ) {
231
			$ts = PageBotList::getValidTimestamp( $groups );
232
			if ( date( 'd/m', $ts ) === date( 'd/m', strtotime( '- 1 days' ) ) &&
233
				isset( $newContent[ $user ]['override'] )
234
			) {
235
				unset( $newContent[ $user ][ 'override' ] );
236
				$removed[] = $user;
237
			}
238
		}
239
240
		if ( $removed ) {
241
			$this->getLogger()->info( 'Removing overrides for users: ' . implode( ', ', $removed ) );
242
		} else {
243
			$this->getLogger()->debug( 'No overrides to remove' );
244
		}
245
246
		return $newContent;
247
	}
248
}
249