Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 287 |
|
0.00% |
0 / 14 |
CRAP | |
0.00% |
0 / 1 |
SpecialWikiSets | |
0.00% |
0 / 287 |
|
0.00% |
0 / 14 |
4970 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getDescription | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
182 | |||
buildMainView | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
30 | |||
buildSetView | |
0.00% |
0 / 71 |
|
0.00% |
0 / 1 |
380 | |||
buildTypeSelector | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
buildTableByList | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
30 | |||
buildDeleteView | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
6 | |||
addEntry | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
doSubmit | |
0.00% |
0 / 99 |
|
0.00% |
0 / 1 |
272 | |||
doDelete | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
showLogFragment | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
showNoPermissionsView | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\CentralAuth\Special; |
4 | |
5 | use LogEventsList; |
6 | use LogPage; |
7 | use ManualLogEntry; |
8 | use MediaWiki\Extension\CentralAuth\CentralAuthWikiListService; |
9 | use MediaWiki\Extension\CentralAuth\WikiSet; |
10 | use MediaWiki\Html\Html; |
11 | use MediaWiki\MainConfigNames; |
12 | use MediaWiki\SpecialPage\SpecialPage; |
13 | use MediaWiki\Title\Title; |
14 | use MediaWiki\Xml\Xml; |
15 | use MediaWiki\Xml\XmlSelect; |
16 | use PermissionsError; |
17 | |
18 | /** |
19 | * Special page to allow to edit "wikisets" which are used to restrict |
20 | * specific global group permissions to certain wikis. |
21 | * |
22 | * @file |
23 | * @ingroup Extensions |
24 | */ |
25 | |
26 | class SpecialWikiSets extends SpecialPage { |
27 | |
28 | /** @var bool */ |
29 | private $mCanEdit; |
30 | |
31 | private CentralAuthWikiListService $wikiListService; |
32 | |
33 | public function __construct( CentralAuthWikiListService $wikiListService ) { |
34 | parent::__construct( 'WikiSets' ); |
35 | |
36 | $this->wikiListService = $wikiListService; |
37 | } |
38 | |
39 | /** |
40 | * @inheritDoc |
41 | */ |
42 | public function getDescription() { |
43 | return $this->msg( 'centralauth-editset' ); |
44 | } |
45 | |
46 | /** |
47 | * @param string|null $subpage |
48 | * @return void |
49 | */ |
50 | public function execute( $subpage ) { |
51 | $this->mCanEdit = $this->getContext()->getAuthority()->isAllowed( 'globalgrouppermissions' ); |
52 | $req = $this->getRequest(); |
53 | $tokenOk = $req->wasPosted() |
54 | && $this->getUser()->matchEditToken( $req->getVal( 'wpEditToken' ) ); |
55 | |
56 | $this->setHeaders(); |
57 | |
58 | if ( $subpage === null ) { |
59 | $this->buildMainView(); |
60 | return; |
61 | } |
62 | |
63 | if ( str_starts_with( $subpage, 'delete/' ) ) { |
64 | if ( !$this->mCanEdit ) { |
65 | $this->showNoPermissionsView(); |
66 | } |
67 | |
68 | // Remove delete/ part |
69 | $subpage = substr( $subpage, 7 ); |
70 | |
71 | if ( is_numeric( $subpage ) ) { |
72 | if ( $tokenOk ) { |
73 | $this->doDelete( $subpage ); |
74 | return; |
75 | } |
76 | |
77 | $this->buildDeleteView( $subpage ); |
78 | return; |
79 | } |
80 | } |
81 | |
82 | $set = null; |
83 | if ( $subpage !== '0' ) { |
84 | $set = is_numeric( $subpage ) ? WikiSet::newFromId( $subpage ) : WikiSet::newFromName( $subpage ); |
85 | if ( !$set ) { |
86 | $this->getOutput()->setPageTitleMsg( $this->msg( 'error' ) ); |
87 | $error = $this->msg( 'centralauth-editset-notfound', $subpage )->escaped(); |
88 | $this->buildMainView( Html::errorBox( $error ) ); |
89 | return; |
90 | } |
91 | } elseif ( !$this->mCanEdit ) { |
92 | $this->showNoPermissionsView(); |
93 | } |
94 | |
95 | if ( $tokenOk ) { |
96 | if ( !$this->mCanEdit ) { |
97 | $this->showNoPermissionsView(); |
98 | } |
99 | |
100 | $this->doSubmit( $set ); |
101 | return; |
102 | } |
103 | |
104 | $this->buildSetView( $set ); |
105 | } |
106 | |
107 | /** |
108 | * @param string|null $msg Output directly as HTML. Caller must escape. |
109 | */ |
110 | private function buildMainView( ?string $msg = null ) { |
111 | // Give grep a chance to find the usages: centralauth-editset-legend-rw, |
112 | // centralauth-editset-legend-ro |
113 | $msgPostfix = $this->mCanEdit ? 'rw' : 'ro'; |
114 | $legend = $this->msg( "centralauth-editset-legend-{$msgPostfix}" )->escaped(); |
115 | $this->getOutput()->addHTML( "<fieldset><legend>{$legend}</legend>" ); |
116 | if ( $msg ) { |
117 | $this->getOutput()->addHTML( $msg ); |
118 | } |
119 | // Give grep a chance to find the usages: centralauth-editset-intro-rw, |
120 | // centralauth-editset-intro-ro |
121 | $this->getOutput()->addWikiMsg( "centralauth-editset-intro-{$msgPostfix}" ); |
122 | $this->getOutput()->addHTML( '<ul>' ); |
123 | |
124 | // Give grep a chance to find the usages: centralauth-editset-item-rw, |
125 | // centralauth-editset-item-ro |
126 | foreach ( WikiSet::getAllWikiSets() as $set ) { |
127 | $text = $this->msg( "centralauth-editset-item-{$msgPostfix}", |
128 | $set->getName(), $set->getID() )->parse(); |
129 | $this->getOutput()->addHTML( "<li>{$text}</li>" ); |
130 | } |
131 | |
132 | if ( $this->mCanEdit ) { |
133 | $target = $this->getPageTitle( '0' ); |
134 | $newlink = $this->getLinkRenderer()->makeLink( |
135 | $target, |
136 | $this->msg( 'centralauth-editset-new' )->text() |
137 | ); |
138 | $this->getOutput()->addHTML( "<li>{$newlink}</li>" ); |
139 | } |
140 | |
141 | $this->getOutput()->addHTML( '</ul></fieldset>' ); |
142 | } |
143 | |
144 | /** |
145 | * @param WikiSet|null $set wiki set to operate on |
146 | * @param bool|string $error False or raw html to output as error |
147 | * @param string|null $name (Optional) Name of WikiSet |
148 | * @param string|null $type WikiSet::OPTIN or WikiSet::OPTOUT |
149 | * @param string[]|null $wikis |
150 | * @param string|null $reason |
151 | */ |
152 | private function buildSetView( |
153 | ?WikiSet $set, $error = false, $name = null, $type = null, $wikis = null, $reason = null |
154 | ) { |
155 | $this->getOutput()->addBacklinkSubtitle( $this->getPageTitle() ); |
156 | |
157 | if ( !$name ) { |
158 | $name = $set ? $set->getName() : ''; |
159 | } |
160 | if ( !$type ) { |
161 | $type = $set ? $set->getType() : WikiSet::OPTIN; |
162 | } |
163 | if ( !$wikis ) { |
164 | $wikis = $set ? $set->getWikisRaw() : []; |
165 | } |
166 | |
167 | sort( $wikis ); |
168 | $wikis = implode( "\n", $wikis ); |
169 | |
170 | $url = $this->getPageTitle( (string)( $set ? $set->getId() : 0 ) ) |
171 | ->getLocalUrl(); |
172 | |
173 | if ( $this->mCanEdit ) { |
174 | // Give grep a chance to find the usages: |
175 | // centralauth-editset-legend-edit, centralauth-editset-legend-new |
176 | $legend = $this->msg( |
177 | 'centralauth-editset-legend-' . ( $set ? 'edit' : 'new' ), |
178 | $name |
179 | )->escaped(); |
180 | } else { |
181 | $legend = $this->msg( 'centralauth-editset-legend-view', $name )->escaped(); |
182 | } |
183 | |
184 | $this->getOutput()->addHTML( "<fieldset><legend>{$legend}</legend>" ); |
185 | |
186 | if ( $set ) { |
187 | $groups = $set->getRestrictedGroups(); |
188 | if ( $groups ) { |
189 | $usage = "<ul>\n"; |
190 | foreach ( $groups as $group ) { |
191 | $usage .= "<li>" . $this->msg( 'centralauth-editset-grouplink', $group ) |
192 | ->parse() . "</li>\n"; |
193 | } |
194 | $usage .= "</ul>"; |
195 | } else { |
196 | $usage = $this->msg( 'centralauth-editset-nouse' )->parseAsBlock(); |
197 | } |
198 | $sortedWikis = $set->getWikisRaw(); |
199 | sort( $sortedWikis ); |
200 | } else { |
201 | $usage = ''; |
202 | $sortedWikis = []; |
203 | } |
204 | |
205 | # Make an array of the opposite list of wikis |
206 | # (all databases *excluding* the defined ones) |
207 | $restWikis = []; |
208 | foreach ( $this->getConfig()->get( MainConfigNames::LocalDatabases ) as $wiki ) { |
209 | if ( !in_array( $wiki, $sortedWikis ) ) { |
210 | $restWikis[] = $wiki; |
211 | } |
212 | } |
213 | sort( $restWikis ); |
214 | |
215 | if ( $this->mCanEdit ) { |
216 | if ( $error ) { |
217 | $this->getOutput()->addHTML( Html::errorBox( $error ) ); |
218 | } |
219 | $this->getOutput()->addHTML( |
220 | Html::openElement( |
221 | 'form', |
222 | [ 'action' => $url, 'method' => 'POST' ] |
223 | ) |
224 | ); |
225 | |
226 | $form = []; |
227 | $form['centralauth-editset-name'] = Xml::input( 'wpName', false, $name ); |
228 | if ( $usage ) { |
229 | $form['centralauth-editset-usage'] = $usage; |
230 | } |
231 | $form['centralauth-editset-type'] = $this->buildTypeSelector( 'wpType', $type ); |
232 | $form['centralauth-editset-wikis'] = Xml::textarea( 'wpWikis', $wikis ); |
233 | $form['centralauth-editset-restwikis'] = Xml::textarea( 'wpRestWikis', |
234 | implode( "\n", $restWikis ), 40, 5, [ 'readonly' => true ] ); |
235 | $form['centralauth-editset-reason'] = Xml::input( 'wpReason', 50, $reason ?? '' ); |
236 | |
237 | $this->getOutput()->addHTML( Xml::buildForm( $form, 'centralauth-editset-submit' ) ); |
238 | |
239 | $edittoken = Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() ); |
240 | $this->getOutput()->addHTML( "<p>{$edittoken}</p></form></fieldset>" ); |
241 | } else { |
242 | // Give grep a chance to find the usages: centralauth-editset-optin, |
243 | // centralauth-editset-optout |
244 | $form = []; |
245 | $form['centralauth-editset-name'] = htmlspecialchars( $name ); |
246 | $form['centralauth-editset-usage'] = $usage; |
247 | $form['centralauth-editset-type'] = $this->msg( "centralauth-editset-{$type}" ) |
248 | ->escaped(); |
249 | $form['centralauth-editset-wikis'] = self::buildTableByList( |
250 | $sortedWikis, 3, [ 'style' => 'width:100%;' ] |
251 | ) . '<hr>'; |
252 | $form['centralauth-editset-restwikis'] = self::buildTableByList( |
253 | $restWikis, 3, [ 'style' => 'width:100%;' ] |
254 | ); |
255 | |
256 | $this->getOutput()->addHTML( Xml::buildForm( $form ) ); |
257 | } |
258 | |
259 | if ( $set ) { |
260 | $this->showLogFragment( (string)$set->getId() ); |
261 | } |
262 | } |
263 | |
264 | /** |
265 | * @param string $name |
266 | * @param string $value |
267 | * @return string |
268 | */ |
269 | private function buildTypeSelector( $name, $value ) { |
270 | // Give grep a chance to find the usages: centralauth-editset-optin, |
271 | // centralauth-editset-optout |
272 | $select = new XmlSelect( $name, 'set-type', $value ); |
273 | foreach ( [ WikiSet::OPTIN, WikiSet::OPTOUT ] as $type ) { |
274 | $select->addOption( $this->msg( "centralauth-editset-{$type}" )->text(), $type ); |
275 | } |
276 | return $select->getHTML(); |
277 | } |
278 | |
279 | /** |
280 | * Builds a table of several columns, and divides the items of |
281 | * $list equally among each column. All items are escaped. |
282 | * |
283 | * Could in the future be replaced by CSS column-count. |
284 | * |
285 | * @param string[] $list |
286 | * @param int $columns number of columns |
287 | * @param array $tableAttribs <table> attributes |
288 | * @return string Table |
289 | */ |
290 | private function buildTableByList( array $list, int $columns = 2, array $tableAttribs = [] ): string { |
291 | $count = count( $list ); |
292 | if ( $count === 0 ) { |
293 | return $this->msg( 'centralauth-editset-nowikis' )->parse(); |
294 | } |
295 | |
296 | # If there are less items than columns, limit the number of columns |
297 | $columns = $count < $columns ? $count : $columns; |
298 | $itemsPerCol = (int)ceil( $count / $columns ); |
299 | $splitLists = array_chunk( $list, $itemsPerCol ); |
300 | |
301 | $body = ''; |
302 | foreach ( $splitLists as $splitList ) { |
303 | $body .= '<td style="width:' . round( 100 / $columns ) . '%;"><ul>'; |
304 | foreach ( $splitList as $listitem ) { |
305 | $body .= Html::element( 'li', [], $listitem ); |
306 | } |
307 | $body .= '</ul></td>'; |
308 | } |
309 | return Html::rawElement( 'table', $tableAttribs, |
310 | '<tbody>' . |
311 | Html::rawElement( 'tr', [ 'style' => 'vertical-align:top;' ], $body ) . |
312 | '</tbody>' |
313 | ); |
314 | } |
315 | |
316 | /** |
317 | * @param string $subpage |
318 | */ |
319 | private function buildDeleteView( $subpage ) { |
320 | $this->getOutput()->addBacklinkSubtitle( $this->getPageTitle() ); |
321 | |
322 | $set = WikiSet::newFromID( $subpage ); |
323 | if ( !$set ) { |
324 | $this->buildMainView( Html::errorBox( $this->msg( 'centralauth-editset-notfound', $subpage )->escaped() ) ); |
325 | return; |
326 | } |
327 | |
328 | $legend = $this->msg( 'centralauth-editset-legend-delete', $set->getName() )->text(); |
329 | $form = [ 'centralauth-editset-reason' => Xml::input( 'wpReason' ) ]; |
330 | $url = $this->getPageTitle( 'delete/' . $subpage )->getLocalUrl(); |
331 | $edittoken = Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() ); |
332 | |
333 | $this->getOutput()->addHTML( |
334 | Html::openElement( 'fieldset' ) . |
335 | Html::element( 'legend', [], $legend ) . |
336 | Html::openElement( 'form', [ 'action' => $url, 'method' => 'post' ] ) |
337 | ); |
338 | $this->getOutput()->addHTML( Xml::buildForm( $form, 'centralauth-editset-submit-delete' ) ); |
339 | $this->getOutput()->addHTML( "<p>{$edittoken}</p></form></fieldset>" ); |
340 | } |
341 | |
342 | /** |
343 | * Log action to 'gblrights' log |
344 | * |
345 | * @param string $action Type of action |
346 | * @param Title $title |
347 | * @param string $reason |
348 | * @param array $params |
349 | */ |
350 | private function addEntry( $action, $title, $reason, $params ): void { |
351 | $entry = new ManualLogEntry( 'gblrights', $action ); |
352 | $entry->setTarget( $title ); |
353 | $entry->setPerformer( $this->getUser() ); |
354 | $entry->setComment( $reason ); |
355 | $entry->setParameters( $params ); |
356 | $logid = $entry->insert(); |
357 | $entry->publish( $logid ); |
358 | } |
359 | |
360 | /** |
361 | * @param WikiSet|null $set wiki set to operate on |
362 | */ |
363 | private function doSubmit( ?WikiSet $set ) { |
364 | $name = $this->getContentLanguage()->ucfirst( $this->getRequest()->getVal( 'wpName' ) ); |
365 | $type = $this->getRequest()->getVal( 'wpType' ); |
366 | $wikis = array_unique( preg_split( |
367 | '/(\s+|\s*\W\s*)/', $this->getRequest()->getVal( 'wpWikis' ), -1, PREG_SPLIT_NO_EMPTY ) |
368 | ); |
369 | $reason = $this->getRequest()->getVal( 'wpReason' ); |
370 | |
371 | if ( !Title::newFromText( $name ) ) { |
372 | $this->buildSetView( $set, $this->msg( 'centralauth-editset-badname' )->escaped(), |
373 | $name, $type, $wikis, $reason ); |
374 | return; |
375 | } |
376 | if ( ( !$set || $set->getName() != $name ) && WikiSet::newFromName( $name ) ) { |
377 | $this->buildSetView( $set, $this->msg( 'centralauth-editset-setexists' )->escaped(), |
378 | $name, $type, $wikis, $reason ); |
379 | return; |
380 | } |
381 | if ( !in_array( $type, [ WikiSet::OPTIN, WikiSet::OPTOUT ] ) ) { |
382 | $this->buildSetView( $set, $this->msg( 'centralauth-editset-badtype' )->escaped(), |
383 | $name, $type, $wikis, $reason ); |
384 | return; |
385 | } |
386 | if ( !$wikis ) { |
387 | $this->buildSetView( $set, $this->msg( 'centralauth-editset-zerowikis' )->escaped(), |
388 | $name, $type, $wikis, $reason ); |
389 | return; |
390 | } |
391 | |
392 | $badwikis = []; |
393 | $allwikis = $this->wikiListService->getWikiList(); |
394 | foreach ( $wikis as $wiki ) { |
395 | if ( !in_array( $wiki, $allwikis ) ) { |
396 | $badwikis[] = $wiki; |
397 | } |
398 | } |
399 | if ( $badwikis ) { |
400 | $this->buildSetView( $set, $this->msg( |
401 | 'centralauth-editset-badwikis', |
402 | implode( ', ', $badwikis ) ) |
403 | ->numParams( count( $badwikis ) ) |
404 | ->escaped(), |
405 | $name, $type, $wikis, $reason |
406 | ); |
407 | return; |
408 | } |
409 | |
410 | if ( $set ) { |
411 | $oldname = $set->getName(); |
412 | $oldtype = $set->getType(); |
413 | $oldwikis = $set->getWikisRaw(); |
414 | } else { |
415 | $set = new WikiSet(); |
416 | $oldname = $oldtype = null; |
417 | $oldwikis = []; |
418 | } |
419 | $set->setName( $name ); |
420 | $set->setType( $type ); |
421 | $set->setWikisRaw( $wikis ); |
422 | $set->saveToDB(); |
423 | |
424 | // Now logging |
425 | $title = $this->getPageTitle( (string)$set->getID() ); |
426 | if ( !$oldname ) { |
427 | // New set |
428 | $this->addEntry( |
429 | 'newset', |
430 | $title, |
431 | $reason, |
432 | [ |
433 | '4::name' => $name, |
434 | '5::type' => $type, |
435 | 'wikis' => $wikis, |
436 | ] |
437 | ); |
438 | } else { |
439 | if ( $oldname != $name ) { |
440 | $this->addEntry( |
441 | 'setrename', |
442 | $title, |
443 | $reason, |
444 | [ |
445 | '4::name' => $name, |
446 | '5::oldName' => $oldname, |
447 | ] |
448 | ); |
449 | } |
450 | if ( $oldtype != $type ) { |
451 | $this->addEntry( |
452 | 'setnewtype', |
453 | $title, |
454 | $reason, |
455 | [ |
456 | '4::name' => $name, |
457 | '5::oldType' => $oldtype, |
458 | '6::type' => $type, |
459 | ] |
460 | ); |
461 | } |
462 | $added = array_diff( $wikis, $oldwikis ); |
463 | $removed = array_diff( $oldwikis, $wikis ); |
464 | if ( $added || $removed ) { |
465 | $this->addEntry( |
466 | 'setchange', |
467 | $title, |
468 | $reason, |
469 | [ |
470 | '4::name' => $name, |
471 | 'added' => $added, |
472 | 'removed' => $removed, |
473 | ] |
474 | ); |
475 | } |
476 | } |
477 | |
478 | $returnLink = $this->getLinkRenderer()->makeKnownLink( |
479 | $this->getPageTitle(), $this->msg( 'centralauth-editset-return' )->text() ); |
480 | |
481 | $this->getOutput()->addHTML( |
482 | Html::successBox( $this->msg( 'centralauth-editset-success' )->escaped() ) . |
483 | '<p>' . $returnLink . '</p>' |
484 | ); |
485 | } |
486 | |
487 | /** |
488 | * @param string $setId |
489 | */ |
490 | private function doDelete( $setId ) { |
491 | $set = WikiSet::newFromID( $setId ); |
492 | if ( !$set ) { |
493 | $this->buildMainView( Html::errorBox( $this->msg( 'centralauth-editset-notfound', $setId )->escaped() ) ); |
494 | return; |
495 | } |
496 | |
497 | $reason = $this->getRequest()->getVal( 'wpReason' ); |
498 | $name = $set->getName(); |
499 | $set->delete(); |
500 | |
501 | $title = $this->getPageTitle( (string)$set->getID() ); |
502 | $this->addEntry( 'deleteset', $title, $reason, [ '4::name' => $name ] ); |
503 | |
504 | $this->buildMainView( Html::successBox( $this->msg( 'centralauth-editset-success-delete' )->escaped() ) ); |
505 | } |
506 | |
507 | /** |
508 | * @param string $number |
509 | */ |
510 | protected function showLogFragment( $number ) { |
511 | $title = $this->getPageTitle( $number ); |
512 | $logPage = new LogPage( 'gblrights' ); |
513 | $out = $this->getOutput(); |
514 | $out->addHTML( Xml::element( 'h2', null, $logPage->getName()->text() . "\n" ) ); |
515 | LogEventsList::showLogExtract( $out, 'gblrights', $title->getPrefixedText() ); |
516 | } |
517 | |
518 | /** @inheritDoc */ |
519 | protected function getGroupName() { |
520 | return 'wiki'; |
521 | } |
522 | |
523 | /** |
524 | * @phan-return never |
525 | * @return void |
526 | * @throws PermissionsError |
527 | */ |
528 | private function showNoPermissionsView() { |
529 | throw new PermissionsError( 'globalgrouppermissions' ); |
530 | } |
531 | } |