use Exception;
use Hooks;
use MediaWiki\Linker\LinkTarget;
+use MediaWiki\Revision\RevisionLookup;
+use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Session\SessionManager;
use MediaWiki\Special\SpecialPageFactory;
use MediaWiki\User\UserIdentity;
use SpecialPage;
use Title;
use User;
+use Wikimedia\ScopedCallback;
use WikiPage;
/**
/** @var SpecialPageFactory */
private $specialPageFactory;
+ /** @var RevisionLookup */
+ private $revisionLookup;
+
/** @var string[] List of pages names anonymous user may see */
private $whitelistRead;
/** @var string[][] Cached user rights */
private $usersRights = null;
+ /**
+ * Temporary user rights, valid for the current request only.
+ * @var string[][][] userid => override group => rights
+ */
+ private $temporaryUserRights = [];
+
/** @var string[] Cached rights for isEveryoneAllowed */
private $cachedRights = [];
'editmyusercss',
'editmyuserjson',
'editmyuserjs',
+ 'editmyuserjsredirect',
'editmywatchlist',
'editsemiprotected',
'editsitecss',
/**
* @param SpecialPageFactory $specialPageFactory
+ * @param RevisionLookup $revisionLookup
* @param string[] $whitelistRead
* @param string[] $whitelistReadRegexp
* @param bool $emailConfirmToEdit
*/
public function __construct(
SpecialPageFactory $specialPageFactory,
+ RevisionLookup $revisionLookup,
$whitelistRead,
$whitelistReadRegexp,
$emailConfirmToEdit,
NamespaceInfo $nsInfo
) {
$this->specialPageFactory = $specialPageFactory;
+ $this->revisionLookup = $revisionLookup;
$this->whitelistRead = $whitelistRead;
$this->whitelistReadRegexp = $whitelistReadRegexp;
$this->emailConfirmToEdit = $emailConfirmToEdit;
&& !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' )
) {
$errors[] = [ 'mycustomjsprotected', $action ];
+ } elseif (
+ $page->isUserJsConfigPage()
+ && !$user->isAllowedAny( 'edituserjs', 'editmyuserjsredirect' )
+ ) {
+ // T207750 - do not allow users to edit a redirect if they couldn't edit the target
+ $rev = $this->revisionLookup->getRevisionByTitle( $page );
+ $content = $rev ? $rev->getContent( 'main', RevisionRecord::RAW ) : null;
+ $target = $content ? $content->getUltimateRedirectTarget() : null;
+ if ( $target && (
+ !$target->inNamespace( NS_USER )
+ || !preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $target->getText() )
+ ) ) {
+ $errors[] = [ 'mycustomjsredirectprotected', $action ];
+ }
}
} else {
// Users need editmyuser* to edit their own CSS/JSON/JS subpages, except for
);
}
}
- return $this->usersRights[ $user->getId() ];
+ $rights = $this->usersRights[ $user->getId() ];
+ foreach ( $this->temporaryUserRights[ $user->getId() ] ?? [] as $overrides ) {
+ $rights = array_values( array_unique( array_merge( $rights, $overrides ) ) );
+ }
+ return $rights;
}
/**
return $this->allRights;
}
+ /**
+ * Add temporary user rights, only valid for the current scope.
+ * This is meant for making it possible to programatically trigger certain actions that
+ * the user wouldn't be able to trigger themselves; e.g. allow users without the bot right
+ * to make bot-flagged actions through certain special pages.
+ * Returns a "scope guard" variable; whenever that variable goes out of scope or is consumed
+ * via ScopedCallback::consume(), the temporary rights are revoked.
+ *
+ * @since 1.34
+ *
+ * @param UserIdentity $user
+ * @param string|string[] $rights
+ * @return ScopedCallback
+ */
+ public function addTemporaryUserRights( UserIdentity $user, $rights ) {
+ $userId = $user->getId();
+ $nextKey = count( $this->temporaryUserRights[$userId] ?? [] );
+ $this->temporaryUserRights[$userId][$nextKey] = (array)$rights;
+ return new ScopedCallback( function () use ( $userId, $nextKey ) {
+ unset( $this->temporaryUserRights[$userId][$nextKey] );
+ } );
+ }
+
/**
* Overrides user permissions cache
*