Remove parameter 'options' from hook 'SkinEditSectionLinks'
[lhc/web/wiklou.git] / includes / Permissions / PermissionManager.php
1 <?php
2 /**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20 namespace MediaWiki\Permissions;
21
22 use Action;
23 use Exception;
24 use FatalError;
25 use Hooks;
26 use MediaWiki\Linker\LinkTarget;
27 use MediaWiki\Special\SpecialPageFactory;
28 use MessageSpecifier;
29 use MWException;
30 use NamespaceInfo;
31 use RequestContext;
32 use SpecialPage;
33 use Title;
34 use User;
35 use WikiPage;
36
37 /**
38 * A service class for checking permissions
39 * To obtain an instance, use MediaWikiServices::getInstance()->getPermissionManager().
40 *
41 * @since 1.33
42 */
43 class PermissionManager {
44
45 /** @var string Does cheap permission checks from replica DBs (usable for GUI creation) */
46 const RIGOR_QUICK = 'quick';
47
48 /** @var string Does cheap and expensive checks possibly from a replica DB */
49 const RIGOR_FULL = 'full';
50
51 /** @var string Does cheap and expensive checks, using the master as needed */
52 const RIGOR_SECURE = 'secure';
53
54 /** @var SpecialPageFactory */
55 private $specialPageFactory;
56
57 /** @var string[] List of pages names anonymous user may see */
58 private $whitelistRead;
59
60 /** @var string[] Whitelists publicly readable titles with regular expressions */
61 private $whitelistReadRegexp;
62
63 /** @var bool Require users to confirm email address before they can edit */
64 private $emailConfirmToEdit;
65
66 /** @var bool If set to true, blocked users will no longer be allowed to log in */
67 private $blockDisablesLogin;
68
69 /**
70 * @param SpecialPageFactory $specialPageFactory
71 * @param string[] $whitelistRead
72 * @param string[] $whitelistReadRegexp
73 * @param bool $emailConfirmToEdit
74 * @param bool $blockDisablesLogin
75 */
76 public function __construct(
77 SpecialPageFactory $specialPageFactory,
78 $whitelistRead,
79 $whitelistReadRegexp,
80 $emailConfirmToEdit,
81 $blockDisablesLogin,
82 NamespaceInfo $nsInfo
83 ) {
84 $this->specialPageFactory = $specialPageFactory;
85 $this->whitelistRead = $whitelistRead;
86 $this->whitelistReadRegexp = $whitelistReadRegexp;
87 $this->emailConfirmToEdit = $emailConfirmToEdit;
88 $this->blockDisablesLogin = $blockDisablesLogin;
89 $this->nsInfo = $nsInfo;
90 }
91
92 /**
93 * Can $user perform $action on a page?
94 *
95 * The method is intended to replace Title::userCan()
96 * The $user parameter need to be superseded by UserIdentity value in future
97 * The $title parameter need to be superseded by PageIdentity value in future
98 *
99 * @see Title::userCan()
100 *
101 * @param string $action
102 * @param User $user
103 * @param LinkTarget $page
104 * @param string $rigor One of PermissionManager::RIGOR_ constants
105 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
106 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
107 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
108 *
109 * @return bool
110 * @throws Exception
111 */
112 public function userCan( $action, User $user, LinkTarget $page, $rigor = self::RIGOR_SECURE ) {
113 return !count( $this->getPermissionErrorsInternal( $action, $user, $page, $rigor, true ) );
114 }
115
116 /**
117 * Can $user perform $action on a page?
118 *
119 * @todo FIXME: This *does not* check throttles (User::pingLimiter()).
120 *
121 * @param string $action Action that permission needs to be checked for
122 * @param User $user User to check
123 * @param LinkTarget $page
124 * @param string $rigor One of PermissionManager::RIGOR_ constants
125 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
126 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
127 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
128 * @param array $ignoreErrors Array of Strings Set this to a list of message keys
129 * whose corresponding errors may be ignored.
130 *
131 * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
132 * @throws Exception
133 */
134 public function getPermissionErrors(
135 $action,
136 User $user,
137 LinkTarget $page,
138 $rigor = self::RIGOR_SECURE,
139 $ignoreErrors = []
140 ) {
141 $errors = $this->getPermissionErrorsInternal( $action, $user, $page, $rigor );
142
143 // Remove the errors being ignored.
144 foreach ( $errors as $index => $error ) {
145 $errKey = is_array( $error ) ? $error[0] : $error;
146
147 if ( in_array( $errKey, $ignoreErrors ) ) {
148 unset( $errors[$index] );
149 }
150 if ( $errKey instanceof MessageSpecifier && in_array( $errKey->getKey(), $ignoreErrors ) ) {
151 unset( $errors[$index] );
152 }
153 }
154
155 return $errors;
156 }
157
158 /**
159 * Check if user is blocked from editing a particular article
160 *
161 * @param User $user
162 * @param LinkTarget $page Title to check
163 * @param bool $fromReplica Whether to check the replica DB instead of the master
164 *
165 * @return bool
166 * @throws FatalError
167 * @throws MWException
168 */
169 public function isBlockedFrom( User $user, LinkTarget $page, $fromReplica = false ) {
170 $blocked = $user->isHidden();
171
172 // TODO: remove upon further migration to LinkTarget
173 $page = Title::newFromLinkTarget( $page );
174
175 if ( !$blocked ) {
176 $block = $user->getBlock( $fromReplica );
177 if ( $block ) {
178 // Special handling for a user's own talk page. The block is not aware
179 // of the user, so this must be done here.
180 if ( $page->equals( $user->getTalkPage() ) ) {
181 $blocked = $block->appliesToUsertalk( $page );
182 } else {
183 $blocked = $block->appliesToTitle( $page );
184 }
185 }
186 }
187
188 // only for the purpose of the hook. We really don't need this here.
189 $allowUsertalk = $user->isAllowUsertalk();
190
191 Hooks::run( 'UserIsBlockedFrom', [ $user, $page, &$blocked, &$allowUsertalk ] );
192
193 return $blocked;
194 }
195
196 /**
197 * Can $user perform $action on a page? This is an internal function,
198 * with multiple levels of checks depending on performance needs; see $rigor below.
199 * It does not check wfReadOnly().
200 *
201 * @param string $action Action that permission needs to be checked for
202 * @param User $user User to check
203 * @param LinkTarget $page
204 * @param string $rigor One of PermissionManager::RIGOR_ constants
205 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
206 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
207 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
208 * @param bool $short Set this to true to stop after the first permission error.
209 *
210 * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
211 * @throws Exception
212 */
213 private function getPermissionErrorsInternal(
214 $action,
215 User $user,
216 LinkTarget $page,
217 $rigor = self::RIGOR_SECURE,
218 $short = false
219 ) {
220 if ( !in_array( $rigor, [ self::RIGOR_QUICK, self::RIGOR_FULL, self::RIGOR_SECURE ] ) ) {
221 throw new Exception( "Invalid rigor parameter '$rigor'." );
222 }
223
224 # Read has special handling
225 if ( $action == 'read' ) {
226 $checks = [
227 'checkPermissionHooks',
228 'checkReadPermissions',
229 'checkUserBlock', // for wgBlockDisablesLogin
230 ];
231 # Don't call checkSpecialsAndNSPermissions, checkSiteConfigPermissions
232 # or checkUserConfigPermissions here as it will lead to duplicate
233 # error messages. This is okay to do since anywhere that checks for
234 # create will also check for edit, and those checks are called for edit.
235 } elseif ( $action == 'create' ) {
236 $checks = [
237 'checkQuickPermissions',
238 'checkPermissionHooks',
239 'checkPageRestrictions',
240 'checkCascadingSourcesRestrictions',
241 'checkActionPermissions',
242 'checkUserBlock'
243 ];
244 } else {
245 $checks = [
246 'checkQuickPermissions',
247 'checkPermissionHooks',
248 'checkSpecialsAndNSPermissions',
249 'checkSiteConfigPermissions',
250 'checkUserConfigPermissions',
251 'checkPageRestrictions',
252 'checkCascadingSourcesRestrictions',
253 'checkActionPermissions',
254 'checkUserBlock'
255 ];
256 }
257
258 $errors = [];
259 foreach ( $checks as $method ) {
260 $errors = $this->$method( $action, $user, $errors, $rigor, $short, $page );
261
262 if ( $short && $errors !== [] ) {
263 break;
264 }
265 }
266
267 return $errors;
268 }
269
270 /**
271 * Check various permission hooks
272 *
273 * @param string $action The action to check
274 * @param User $user User to check
275 * @param array $errors List of current errors
276 * @param string $rigor One of PermissionManager::RIGOR_ constants
277 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
278 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
279 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
280 * @param bool $short Short circuit on first error
281 *
282 * @param LinkTarget $page
283 *
284 * @return array List of errors
285 * @throws FatalError
286 * @throws MWException
287 */
288 private function checkPermissionHooks(
289 $action,
290 User $user,
291 $errors,
292 $rigor,
293 $short,
294 LinkTarget $page
295 ) {
296 // TODO: remove when LinkTarget usage will expand further
297 $page = Title::newFromLinkTarget( $page );
298 // Use getUserPermissionsErrors instead
299 $result = '';
300 if ( !Hooks::run( 'userCan', [ &$page, &$user, $action, &$result ] ) ) {
301 return $result ? [] : [ [ 'badaccess-group0' ] ];
302 }
303 // Check getUserPermissionsErrors hook
304 if ( !Hooks::run( 'getUserPermissionsErrors', [ &$page, &$user, $action, &$result ] ) ) {
305 $errors = $this->resultToError( $errors, $result );
306 }
307 // Check getUserPermissionsErrorsExpensive hook
308 if (
309 $rigor !== self::RIGOR_QUICK
310 && !( $short && count( $errors ) > 0 )
311 && !Hooks::run( 'getUserPermissionsErrorsExpensive', [ &$page, &$user, $action, &$result ] )
312 ) {
313 $errors = $this->resultToError( $errors, $result );
314 }
315
316 return $errors;
317 }
318
319 /**
320 * Add the resulting error code to the errors array
321 *
322 * @param array $errors List of current errors
323 * @param array $result Result of errors
324 *
325 * @return array List of errors
326 */
327 private function resultToError( $errors, $result ) {
328 if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) {
329 // A single array representing an error
330 $errors[] = $result;
331 } elseif ( is_array( $result ) && is_array( $result[0] ) ) {
332 // A nested array representing multiple errors
333 $errors = array_merge( $errors, $result );
334 } elseif ( $result !== '' && is_string( $result ) ) {
335 // A string representing a message-id
336 $errors[] = [ $result ];
337 } elseif ( $result instanceof MessageSpecifier ) {
338 // A message specifier representing an error
339 $errors[] = [ $result ];
340 } elseif ( $result === false ) {
341 // a generic "We don't want them to do that"
342 $errors[] = [ 'badaccess-group0' ];
343 }
344 return $errors;
345 }
346
347 /**
348 * Check that the user is allowed to read this page.
349 *
350 * @param string $action The action to check
351 * @param User $user User to check
352 * @param array $errors List of current errors
353 * @param string $rigor One of PermissionManager::RIGOR_ constants
354 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
355 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
356 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
357 * @param bool $short Short circuit on first error
358 *
359 * @param LinkTarget $page
360 *
361 * @return array List of errors
362 * @throws FatalError
363 * @throws MWException
364 */
365 private function checkReadPermissions(
366 $action,
367 User $user,
368 $errors,
369 $rigor,
370 $short,
371 LinkTarget $page
372 ) {
373 // TODO: remove when LinkTarget usage will expand further
374 $page = Title::newFromLinkTarget( $page );
375
376 $whitelisted = false;
377 if ( User::isEveryoneAllowed( 'read' ) ) {
378 # Shortcut for public wikis, allows skipping quite a bit of code
379 $whitelisted = true;
380 } elseif ( $user->isAllowed( 'read' ) ) {
381 # If the user is allowed to read pages, he is allowed to read all pages
382 $whitelisted = true;
383 } elseif ( $this->isSameSpecialPage( 'Userlogin', $page )
384 || $this->isSameSpecialPage( 'PasswordReset', $page )
385 || $this->isSameSpecialPage( 'Userlogout', $page )
386 ) {
387 # Always grant access to the login page.
388 # Even anons need to be able to log in.
389 $whitelisted = true;
390 } elseif ( is_array( $this->whitelistRead ) && count( $this->whitelistRead ) ) {
391 # Time to check the whitelist
392 # Only do these checks is there's something to check against
393 $name = $page->getPrefixedText();
394 $dbName = $page->getPrefixedDBkey();
395
396 // Check for explicit whitelisting with and without underscores
397 if ( in_array( $name, $this->whitelistRead, true )
398 || in_array( $dbName, $this->whitelistRead, true ) ) {
399 $whitelisted = true;
400 } elseif ( $page->getNamespace() == NS_MAIN ) {
401 # Old settings might have the title prefixed with
402 # a colon for main-namespace pages
403 if ( in_array( ':' . $name, $this->whitelistRead ) ) {
404 $whitelisted = true;
405 }
406 } elseif ( $page->isSpecialPage() ) {
407 # If it's a special page, ditch the subpage bit and check again
408 $name = $page->getDBkey();
409 list( $name, /* $subpage */ ) =
410 $this->specialPageFactory->resolveAlias( $name );
411 if ( $name ) {
412 $pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
413 if ( in_array( $pure, $this->whitelistRead, true ) ) {
414 $whitelisted = true;
415 }
416 }
417 }
418 }
419
420 if ( !$whitelisted && is_array( $this->whitelistReadRegexp )
421 && !empty( $this->whitelistReadRegexp ) ) {
422 $name = $page->getPrefixedText();
423 // Check for regex whitelisting
424 foreach ( $this->whitelistReadRegexp as $listItem ) {
425 if ( preg_match( $listItem, $name ) ) {
426 $whitelisted = true;
427 break;
428 }
429 }
430 }
431
432 if ( !$whitelisted ) {
433 # If the title is not whitelisted, give extensions a chance to do so...
434 Hooks::run( 'TitleReadWhitelist', [ $page, $user, &$whitelisted ] );
435 if ( !$whitelisted ) {
436 $errors[] = $this->missingPermissionError( $action, $short );
437 }
438 }
439
440 return $errors;
441 }
442
443 /**
444 * Get a description array when the user doesn't have the right to perform
445 * $action (i.e. when User::isAllowed() returns false)
446 *
447 * @param string $action The action to check
448 * @param bool $short Short circuit on first error
449 * @return array Array containing an error message key and any parameters
450 */
451 private function missingPermissionError( $action, $short ) {
452 // We avoid expensive display logic for quickUserCan's and such
453 if ( $short ) {
454 return [ 'badaccess-group0' ];
455 }
456
457 // TODO: it would be a good idea to replace the method below with something else like
458 // maybe callback injection
459 return User::newFatalPermissionDeniedStatus( $action )->getErrorsArray()[0];
460 }
461
462 /**
463 * Returns true if this title resolves to the named special page
464 *
465 * @param string $name The special page name
466 * @param LinkTarget $page
467 *
468 * @return bool
469 */
470 private function isSameSpecialPage( $name, LinkTarget $page ) {
471 if ( $page->getNamespace() == NS_SPECIAL ) {
472 list( $thisName, /* $subpage */ ) =
473 $this->specialPageFactory->resolveAlias( $page->getDBkey() );
474 if ( $name == $thisName ) {
475 return true;
476 }
477 }
478 return false;
479 }
480
481 /**
482 * Check that the user isn't blocked from editing.
483 *
484 * @param string $action The action to check
485 * @param User $user User to check
486 * @param array $errors List of current errors
487 * @param string $rigor One of PermissionManager::RIGOR_ constants
488 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
489 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
490 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
491 * @param bool $short Short circuit on first error
492 *
493 * @param LinkTarget $page
494 *
495 * @return array List of errors
496 * @throws MWException
497 */
498 private function checkUserBlock(
499 $action,
500 User $user,
501 $errors,
502 $rigor,
503 $short,
504 LinkTarget $page
505 ) {
506 // Account creation blocks handled at userlogin.
507 // Unblocking handled in SpecialUnblock
508 if ( $rigor === self::RIGOR_QUICK || in_array( $action, [ 'createaccount', 'unblock' ] ) ) {
509 return $errors;
510 }
511
512 // Optimize for a very common case
513 if ( $action === 'read' && !$this->blockDisablesLogin ) {
514 return $errors;
515 }
516
517 if ( $this->emailConfirmToEdit
518 && !$user->isEmailConfirmed()
519 && $action === 'edit'
520 ) {
521 $errors[] = [ 'confirmedittext' ];
522 }
523
524 $useReplica = ( $rigor !== self::RIGOR_SECURE );
525 $block = $user->getBlock( $useReplica );
526
527 // If the user does not have a block, or the block they do have explicitly
528 // allows the action (like "read" or "upload").
529 if ( !$block || $block->appliesToRight( $action ) === false ) {
530 return $errors;
531 }
532
533 // Determine if the user is blocked from this action on this page.
534 // What gets passed into this method is a user right, not an action name.
535 // There is no way to instantiate an action by restriction. However, this
536 // will get the action where the restriction is the same. This may result
537 // in actions being blocked that shouldn't be.
538 $actionObj = null;
539 if ( Action::exists( $action ) ) {
540 // TODO: this drags a ton of dependencies in, would be good to avoid WikiPage
541 // instantiation and decouple it creating an ActionPermissionChecker interface
542 $wikiPage = WikiPage::factory( Title::newFromLinkTarget( $page, 'clone' ) );
543 // Creating an action will perform several database queries to ensure that
544 // the action has not been overridden by the content type.
545 // FIXME: avoid use of RequestContext since it drags in User and Title dependencies
546 // probably we may use fake context object since it's unlikely that Action uses it
547 // anyway. It would be nice if we could avoid instantiating the Action at all.
548 $actionObj = Action::factory( $action, $wikiPage, RequestContext::getMain() );
549 // Ensure that the retrieved action matches the restriction.
550 if ( $actionObj && $actionObj->getRestriction() !== $action ) {
551 $actionObj = null;
552 }
553 }
554
555 // If no action object is returned, assume that the action requires unblock
556 // which is the default.
557 if ( !$actionObj || $actionObj->requiresUnblock() ) {
558 if ( $this->isBlockedFrom( $user, $page, $useReplica ) ) {
559 // @todo FIXME: Pass the relevant context into this function.
560 $errors[] = $block->getPermissionsError( RequestContext::getMain() );
561 }
562 }
563
564 return $errors;
565 }
566
567 /**
568 * Permissions checks that fail most often, and which are easiest to test.
569 *
570 * @param string $action The action to check
571 * @param User $user User to check
572 * @param array $errors List of current errors
573 * @param string $rigor One of PermissionManager::RIGOR_ constants
574 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
575 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
576 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
577 * @param bool $short Short circuit on first error
578 *
579 * @param LinkTarget $page
580 *
581 * @return array List of errors
582 * @throws FatalError
583 * @throws MWException
584 */
585 private function checkQuickPermissions(
586 $action,
587 User $user,
588 $errors,
589 $rigor,
590 $short,
591 LinkTarget $page
592 ) {
593 // TODO: remove when LinkTarget usage will expand further
594 $page = Title::newFromLinkTarget( $page );
595
596 if ( !Hooks::run( 'TitleQuickPermissions',
597 [ $page, $user, $action, &$errors, ( $rigor !== self::RIGOR_QUICK ), $short ] )
598 ) {
599 return $errors;
600 }
601
602 $isSubPage = $this->nsInfo->hasSubpages( $page->getNamespace() ) ?
603 strpos( $page->getText(), '/' ) !== false : false;
604
605 if ( $action == 'create' ) {
606 if (
607 ( $this->nsInfo->isTalk( $page->getNamespace() ) &&
608 !$user->isAllowed( 'createtalk' ) ) ||
609 ( !$this->nsInfo->isTalk( $page->getNamespace() ) &&
610 !$user->isAllowed( 'createpage' ) )
611 ) {
612 $errors[] = $user->isAnon() ? [ 'nocreatetext' ] : [ 'nocreate-loggedin' ];
613 }
614 } elseif ( $action == 'move' ) {
615 if ( !$user->isAllowed( 'move-rootuserpages' )
616 && $page->getNamespace() == NS_USER && !$isSubPage ) {
617 // Show user page-specific message only if the user can move other pages
618 $errors[] = [ 'cant-move-user-page' ];
619 }
620
621 // Check if user is allowed to move files if it's a file
622 if ( $page->getNamespace() == NS_FILE && !$user->isAllowed( 'movefile' ) ) {
623 $errors[] = [ 'movenotallowedfile' ];
624 }
625
626 // Check if user is allowed to move category pages if it's a category page
627 if ( $page->getNamespace() == NS_CATEGORY && !$user->isAllowed( 'move-categorypages' ) ) {
628 $errors[] = [ 'cant-move-category-page' ];
629 }
630
631 if ( !$user->isAllowed( 'move' ) ) {
632 // User can't move anything
633 $userCanMove = User::groupHasPermission( 'user', 'move' );
634 $autoconfirmedCanMove = User::groupHasPermission( 'autoconfirmed', 'move' );
635 if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
636 // custom message if logged-in users without any special rights can move
637 $errors[] = [ 'movenologintext' ];
638 } else {
639 $errors[] = [ 'movenotallowed' ];
640 }
641 }
642 } elseif ( $action == 'move-target' ) {
643 if ( !$user->isAllowed( 'move' ) ) {
644 // User can't move anything
645 $errors[] = [ 'movenotallowed' ];
646 } elseif ( !$user->isAllowed( 'move-rootuserpages' )
647 && $page->getNamespace() == NS_USER && !$isSubPage ) {
648 // Show user page-specific message only if the user can move other pages
649 $errors[] = [ 'cant-move-to-user-page' ];
650 } elseif ( !$user->isAllowed( 'move-categorypages' )
651 && $page->getNamespace() == NS_CATEGORY ) {
652 // Show category page-specific message only if the user can move other pages
653 $errors[] = [ 'cant-move-to-category-page' ];
654 }
655 } elseif ( !$user->isAllowed( $action ) ) {
656 $errors[] = $this->missingPermissionError( $action, $short );
657 }
658
659 return $errors;
660 }
661
662 /**
663 * Check against page_restrictions table requirements on this
664 * page. The user must possess all required rights for this
665 * action.
666 *
667 * @param string $action The action to check
668 * @param User $user User to check
669 * @param array $errors List of current errors
670 * @param string $rigor One of PermissionManager::RIGOR_ constants
671 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
672 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
673 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
674 * @param bool $short Short circuit on first error
675 *
676 * @param LinkTarget $page
677 *
678 * @return array List of errors
679 */
680 private function checkPageRestrictions(
681 $action,
682 User $user,
683 $errors,
684 $rigor,
685 $short,
686 LinkTarget $page
687 ) {
688 // TODO: remove & rework upon further use of LinkTarget
689 $page = Title::newFromLinkTarget( $page );
690 foreach ( $page->getRestrictions( $action ) as $right ) {
691 // Backwards compatibility, rewrite sysop -> editprotected
692 if ( $right == 'sysop' ) {
693 $right = 'editprotected';
694 }
695 // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
696 if ( $right == 'autoconfirmed' ) {
697 $right = 'editsemiprotected';
698 }
699 if ( $right == '' ) {
700 continue;
701 }
702 if ( !$user->isAllowed( $right ) ) {
703 $errors[] = [ 'protectedpagetext', $right, $action ];
704 } elseif ( $page->areRestrictionsCascading() && !$user->isAllowed( 'protect' ) ) {
705 $errors[] = [ 'protectedpagetext', 'protect', $action ];
706 }
707 }
708
709 return $errors;
710 }
711
712 /**
713 * Check restrictions on cascading pages.
714 *
715 * @param string $action The action to check
716 * @param User $user User to check
717 * @param array $errors List of current errors
718 * @param string $rigor One of PermissionManager::RIGOR_ constants
719 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
720 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
721 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
722 * @param bool $short Short circuit on first error
723 *
724 * @param LinkTarget $page
725 *
726 * @return array List of errors
727 */
728 private function checkCascadingSourcesRestrictions(
729 $action,
730 User $user,
731 $errors,
732 $rigor,
733 $short,
734 LinkTarget $page
735 ) {
736 // TODO: remove & rework upon further use of LinkTarget
737 $page = Title::newFromLinkTarget( $page );
738 if ( $rigor !== self::RIGOR_QUICK && !$page->isUserConfigPage() ) {
739 # We /could/ use the protection level on the source page, but it's
740 # fairly ugly as we have to establish a precedence hierarchy for pages
741 # included by multiple cascade-protected pages. So just restrict
742 # it to people with 'protect' permission, as they could remove the
743 # protection anyway.
744 list( $cascadingSources, $restrictions ) = $page->getCascadeProtectionSources();
745 # Cascading protection depends on more than this page...
746 # Several cascading protected pages may include this page...
747 # Check each cascading level
748 # This is only for protection restrictions, not for all actions
749 if ( isset( $restrictions[$action] ) ) {
750 foreach ( $restrictions[$action] as $right ) {
751 // Backwards compatibility, rewrite sysop -> editprotected
752 if ( $right == 'sysop' ) {
753 $right = 'editprotected';
754 }
755 // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
756 if ( $right == 'autoconfirmed' ) {
757 $right = 'editsemiprotected';
758 }
759 if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) {
760 $wikiPages = '';
761 foreach ( $cascadingSources as $wikiPage ) {
762 $wikiPages .= '* [[:' . $wikiPage->getPrefixedText() . "]]\n";
763 }
764 $errors[] = [ 'cascadeprotected', count( $cascadingSources ), $wikiPages, $action ];
765 }
766 }
767 }
768 }
769
770 return $errors;
771 }
772
773 /**
774 * Check action permissions not already checked in checkQuickPermissions
775 *
776 * @param string $action The action to check
777 * @param User $user User to check
778 * @param array $errors List of current errors
779 * @param string $rigor One of PermissionManager::RIGOR_ constants
780 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
781 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
782 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
783 * @param bool $short Short circuit on first error
784 *
785 * @param LinkTarget $page
786 *
787 * @return array List of errors
788 * @throws Exception
789 */
790 private function checkActionPermissions(
791 $action,
792 User $user,
793 $errors,
794 $rigor,
795 $short,
796 LinkTarget $page
797 ) {
798 global $wgDeleteRevisionsLimit, $wgLang;
799
800 // TODO: remove & rework upon further use of LinkTarget
801 $page = Title::newFromLinkTarget( $page );
802
803 if ( $action == 'protect' ) {
804 if ( count( $this->getPermissionErrorsInternal( 'edit', $user, $page, $rigor, true ) ) ) {
805 // If they can't edit, they shouldn't protect.
806 $errors[] = [ 'protect-cantedit' ];
807 }
808 } elseif ( $action == 'create' ) {
809 $title_protection = $page->getTitleProtection();
810 if ( $title_protection ) {
811 if ( $title_protection['permission'] == ''
812 || !$user->isAllowed( $title_protection['permission'] )
813 ) {
814 $errors[] = [
815 'titleprotected',
816 // TODO: get rid of the User dependency
817 User::whoIs( $title_protection['user'] ),
818 $title_protection['reason']
819 ];
820 }
821 }
822 } elseif ( $action == 'move' ) {
823 // Check for immobile pages
824 if ( !$this->nsInfo->isMovable( $page->getNamespace() ) ) {
825 // Specific message for this case
826 $errors[] = [ 'immobile-source-namespace', $page->getNsText() ];
827 } elseif ( !$page->isMovable() ) {
828 // Less specific message for rarer cases
829 $errors[] = [ 'immobile-source-page' ];
830 }
831 } elseif ( $action == 'move-target' ) {
832 if ( !$this->nsInfo->isMovable( $page->getNamespace() ) ) {
833 $errors[] = [ 'immobile-target-namespace', $page->getNsText() ];
834 } elseif ( !$page->isMovable() ) {
835 $errors[] = [ 'immobile-target-page' ];
836 }
837 } elseif ( $action == 'delete' ) {
838 $tempErrors = $this->checkPageRestrictions( 'edit', $user, [], $rigor, true, $page );
839 if ( !$tempErrors ) {
840 $tempErrors = $this->checkCascadingSourcesRestrictions( 'edit',
841 $user, $tempErrors, $rigor, true, $page );
842 }
843 if ( $tempErrors ) {
844 // If protection keeps them from editing, they shouldn't be able to delete.
845 $errors[] = [ 'deleteprotected' ];
846 }
847 if ( $rigor !== self::RIGOR_QUICK && $wgDeleteRevisionsLimit
848 && !$this->userCan( 'bigdelete', $user, $page ) && $page->isBigDeletion()
849 ) {
850 $errors[] = [ 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ];
851 }
852 } elseif ( $action === 'undelete' ) {
853 if ( count( $this->getPermissionErrorsInternal( 'edit', $user, $page, $rigor, true ) ) ) {
854 // Undeleting implies editing
855 $errors[] = [ 'undelete-cantedit' ];
856 }
857 if ( !$page->exists()
858 && count( $this->getPermissionErrorsInternal( 'create', $user, $page, $rigor, true ) )
859 ) {
860 // Undeleting where nothing currently exists implies creating
861 $errors[] = [ 'undelete-cantcreate' ];
862 }
863 }
864 return $errors;
865 }
866
867 /**
868 * Check permissions on special pages & namespaces
869 *
870 * @param string $action The action to check
871 * @param User $user User to check
872 * @param array $errors List of current errors
873 * @param string $rigor One of PermissionManager::RIGOR_ constants
874 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
875 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
876 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
877 * @param bool $short Short circuit on first error
878 *
879 * @param LinkTarget $page
880 *
881 * @return array List of errors
882 */
883 private function checkSpecialsAndNSPermissions(
884 $action,
885 User $user,
886 $errors,
887 $rigor,
888 $short,
889 LinkTarget $page
890 ) {
891 // TODO: remove & rework upon further use of LinkTarget
892 $page = Title::newFromLinkTarget( $page );
893
894 # Only 'createaccount' can be performed on special pages,
895 # which don't actually exist in the DB.
896 if ( $page->getNamespace() == NS_SPECIAL && $action !== 'createaccount' ) {
897 $errors[] = [ 'ns-specialprotected' ];
898 }
899
900 # Check $wgNamespaceProtection for restricted namespaces
901 if ( $page->isNamespaceProtected( $user ) ) {
902 $ns = $page->getNamespace() == NS_MAIN ?
903 wfMessage( 'nstab-main' )->text() : $page->getNsText();
904 $errors[] = $page->getNamespace() == NS_MEDIAWIKI ?
905 [ 'protectedinterface', $action ] : [ 'namespaceprotected', $ns, $action ];
906 }
907
908 return $errors;
909 }
910
911 /**
912 * Check sitewide CSS/JSON/JS permissions
913 *
914 * @param string $action The action to check
915 * @param User $user User to check
916 * @param array $errors List of current errors
917 * @param string $rigor One of PermissionManager::RIGOR_ constants
918 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
919 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
920 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
921 * @param bool $short Short circuit on first error
922 *
923 * @param LinkTarget $page
924 *
925 * @return array List of errors
926 */
927 private function checkSiteConfigPermissions(
928 $action,
929 User $user,
930 $errors,
931 $rigor,
932 $short,
933 LinkTarget $page
934 ) {
935 // TODO: remove & rework upon further use of LinkTarget
936 $page = Title::newFromLinkTarget( $page );
937
938 if ( $action != 'patrol' ) {
939 $error = null;
940 // Sitewide CSS/JSON/JS changes, like all NS_MEDIAWIKI changes, also require the
941 // editinterface right. That's implemented as a restriction so no check needed here.
942 if ( $page->isSiteCssConfigPage() && !$user->isAllowed( 'editsitecss' ) ) {
943 $error = [ 'sitecssprotected', $action ];
944 } elseif ( $page->isSiteJsonConfigPage() && !$user->isAllowed( 'editsitejson' ) ) {
945 $error = [ 'sitejsonprotected', $action ];
946 } elseif ( $page->isSiteJsConfigPage() && !$user->isAllowed( 'editsitejs' ) ) {
947 $error = [ 'sitejsprotected', $action ];
948 } elseif ( $page->isRawHtmlMessage() ) {
949 // Raw HTML can be used to deploy CSS or JS so require rights for both.
950 if ( !$user->isAllowed( 'editsitejs' ) ) {
951 $error = [ 'sitejsprotected', $action ];
952 } elseif ( !$user->isAllowed( 'editsitecss' ) ) {
953 $error = [ 'sitecssprotected', $action ];
954 }
955 }
956
957 if ( $error ) {
958 if ( $user->isAllowed( 'editinterface' ) ) {
959 // Most users / site admins will probably find out about the new, more restrictive
960 // permissions by failing to edit something. Give them more info.
961 // TODO remove this a few release cycles after 1.32
962 $error = [ 'interfaceadmin-info', wfMessage( $error[0], $error[1] ) ];
963 }
964 $errors[] = $error;
965 }
966 }
967
968 return $errors;
969 }
970
971 /**
972 * Check CSS/JSON/JS sub-page permissions
973 *
974 * @param string $action The action to check
975 * @param User $user User to check
976 * @param array $errors List of current errors
977 * @param string $rigor One of PermissionManager::RIGOR_ constants
978 * - RIGOR_QUICK : does cheap permission checks from replica DBs (usable for GUI creation)
979 * - RIGOR_FULL : does cheap and expensive checks possibly from a replica DB
980 * - RIGOR_SECURE : does cheap and expensive checks, using the master as needed
981 * @param bool $short Short circuit on first error
982 *
983 * @param LinkTarget $page
984 *
985 * @return array List of errors
986 */
987 private function checkUserConfigPermissions(
988 $action,
989 User $user,
990 $errors,
991 $rigor,
992 $short,
993 LinkTarget $page
994 ) {
995 // TODO: remove & rework upon further use of LinkTarget
996 $page = Title::newFromLinkTarget( $page );
997
998 # Protect css/json/js subpages of user pages
999 # XXX: this might be better using restrictions
1000
1001 if ( $action === 'patrol' ) {
1002 return $errors;
1003 }
1004
1005 if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $page->getText() ) ) {
1006 // Users need editmyuser* to edit their own CSS/JSON/JS subpages.
1007 if (
1008 $page->isUserCssConfigPage()
1009 && !$user->isAllowedAny( 'editmyusercss', 'editusercss' )
1010 ) {
1011 $errors[] = [ 'mycustomcssprotected', $action ];
1012 } elseif (
1013 $page->isUserJsonConfigPage()
1014 && !$user->isAllowedAny( 'editmyuserjson', 'edituserjson' )
1015 ) {
1016 $errors[] = [ 'mycustomjsonprotected', $action ];
1017 } elseif (
1018 $page->isUserJsConfigPage()
1019 && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' )
1020 ) {
1021 $errors[] = [ 'mycustomjsprotected', $action ];
1022 }
1023 } else {
1024 // Users need editmyuser* to edit their own CSS/JSON/JS subpages, except for
1025 // deletion/suppression which cannot be used for attacks and we want to avoid the
1026 // situation where an unprivileged user can post abusive content on their subpages
1027 // and only very highly privileged users could remove it.
1028 if ( !in_array( $action, [ 'delete', 'deleterevision', 'suppressrevision' ], true ) ) {
1029 if (
1030 $page->isUserCssConfigPage()
1031 && !$user->isAllowed( 'editusercss' )
1032 ) {
1033 $errors[] = [ 'customcssprotected', $action ];
1034 } elseif (
1035 $page->isUserJsonConfigPage()
1036 && !$user->isAllowed( 'edituserjson' )
1037 ) {
1038 $errors[] = [ 'customjsonprotected', $action ];
1039 } elseif (
1040 $page->isUserJsConfigPage()
1041 && !$user->isAllowed( 'edituserjs' )
1042 ) {
1043 $errors[] = [ 'customjsprotected', $action ];
1044 }
1045 }
1046 }
1047
1048 return $errors;
1049 }
1050
1051 }