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