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