refactor: Change checking of unscoping permissions

Instead of checking the page content for a raw `wrapclass` string, look up the current revision's user and check if that user has the permission to enable un-scoping.

This _is_ expensive as each time the tag is handled, a lookup will occur.

In reference to #18
This commit is contained in:
H. C. Kruse 2024-03-31 20:31:55 +02:00
parent 3e73f15057
commit 202add59e6
No known key found for this signature in database
GPG key ID: 0EADE441303A09B4
3 changed files with 35 additions and 71 deletions

View file

@ -33,6 +33,8 @@ Enables or disables css variable support.
Default: `false`
Allows users with `editinterface` permissions to unscope css by setting a `wrapclass` attribute.
**Note**: This is potentially expensive, as each templatestyles tag with `wrapclass` set, will attempt to look up the user of the current page revision, and check if this user has the permission to activate css un-scoping.
Example:
`<templatestyles src="Foo/style.css" wrapclass="mediawiki" />` results in the css being scoped to `.mediawiki` instead of `.mw-parser-output`.

View file

@ -48,18 +48,13 @@
},
"HookHandlers": {
"MainHooks": {
"class": "MediaWiki\\Extension\\TemplateStylesExtender\\Hooks\\MainHooks",
"services": [
"PermissionManager",
"UserFactory"
]
"class": "MediaWiki\\Extension\\TemplateStylesExtender\\Hooks\\MainHooks"
}
},
"Hooks": {
"TemplateStylesPropertySanitizer": "MediaWiki\\Extension\\TemplateStylesExtender\\Hooks\\PropertySanitizerHook::onSanitize",
"TemplateStylesStylesheetSanitizer": "MediaWiki\\Extension\\TemplateStylesExtender\\Hooks\\StylesheetSanitizerHook::onSanitize",
"ParserFirstCallInit": "MainHooks",
"EditPage::attemptSave": "MainHooks"
"ParserFirstCallInit": "MainHooks"
},
"manifest_version": 2
}

View file

@ -2,42 +2,19 @@
namespace MediaWiki\Extension\TemplateStylesExtender\Hooks;
use MediaWiki\EditPage\EditPage;
use MediaWiki\Extension\TemplateStyles\Hooks;
use MediaWiki\Extension\TemplateStylesExtender\TemplateStylesExtender;
use MediaWiki\Hook\EditPage__attemptSaveHook;
use MediaWiki\Hook\ParserFirstCallInitHook;
use MediaWiki\Permissions\PermissionManager;
use MediaWiki\Revision\SlotRecord;
use MediaWiki\User\UserFactory;
use MediaWiki\MediaWikiServices;
use MediaWiki\User\UserIdentity;
use MWException;
use Parser;
use PermissionsError;
use PPFrame;
/**
* phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName
*/
class MainHooks implements ParserFirstCallInitHook, EditPage__attemptSaveHook {
/**
* @var PermissionManager
*/
private PermissionManager $manager;
/**
* @var UserFactory
*/
private UserFactory $factory;
/**
* @param PermissionManager $manager
* @param UserFactory $factory
*/
public function __construct( PermissionManager $manager, UserFactory $factory ) {
$this->manager = $manager;
$this->factory = $factory;
}
class MainHooks implements ParserFirstCallInitHook {
/**
* @param Parser $parser
@ -51,6 +28,9 @@ class MainHooks implements ParserFirstCallInitHook, EditPage__attemptSaveHook {
* This is a wrapper for <templatestyles> tags,
* that allows unscoping of css for users with 'editinterface' permissions
*
* Note this is a potentially expensive operation, as a lookup for the user of the current revision is done.
* The unscoping will only happen, if the editor of the current revision has the rights to do so
*
* @param string $text
* @param string[] $params
* @param Parser $parser
@ -59,17 +39,40 @@ class MainHooks implements ParserFirstCallInitHook, EditPage__attemptSaveHook {
* @see Hooks::handleTag()
*/
public static function handleTag( $text, $params, $parser, $frame ): string {
$getOutput = static fn() => Hooks::handleTag( $text, $params, $parser, $frame );
if (
!isset( $params['wrapclass'] ) ||
$parser->getOptions() === null ||
!TemplateStylesExtender::getConfigValue( 'TemplateStylesExtenderEnableUnscopingSupport' )
) {
return Hooks::handleTag( $text, $params, $parser, $frame );
return $getOutput();
}
// 'wrapclass' option is set...
/** @var \ParserOptions $options - Fix typehint */
$options = $parser->getOptions();
$wrapClass = $options->getWrapOutputClass();
if ( isset( $params['wrapclass'] ) ) {
$permission = TemplateStylesExtender::getConfigValue( 'TemplateStylesExtenderUnscopingPermission' );
$rev = MediaWikiServices::getInstance()->getRevisionLookup()->getRevisionByTitle( $frame->getTitle() );
if ( $rev === null || $rev->getUser() === null ) {
return $getOutput();
}
/** @var UserIdentity $user - Fix typehint */
$user = $rev->getUser();
$user = MediaWikiServices::getInstance()->getUserFactory()->newFromUserIdentity( $user );
$permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
$userCan = $permissionManager->userHasRight( $user, $permission )
|| $permissionManager->userCan( $permission, $user, $frame->getTitle() );
// If the editor of the last revision is allowed to change the wrapclass, set it
if ( $userCan ) {
$options->setOption( 'wrapclass', $params['wrapclass'] );
}
@ -78,40 +81,4 @@ class MainHooks implements ParserFirstCallInitHook, EditPage__attemptSaveHook {
return $out;
}
/**
* Check if 'wrapclass' was used in the page, if so only users with 'editinterface' permissions may save the page
*
* @param EditPage $editpage_Obj
* @return true
* @throws PermissionsError
*/
public function onEditPage__attemptSave( $editpage_Obj ): bool {
$revision = $editpage_Obj->getExpectedParentRevision();
if (
$revision === null ||
!TemplateStylesExtender::getConfigValue( 'TemplateStylesExtenderEnableUnscopingSupport' )
) {
return true;
}
$content = $revision->getContent( SlotRecord::MAIN );
if ( $content === null ) {
return true;
}
$permission = TemplateStylesExtender::getConfigValue( 'TemplateStylesExtenderUnscopingPermission' );
$user = $this->factory->newFromUserIdentity( $editpage_Obj->getContext()->getUser() );
$userCan = $this->manager->userHasRight( $user, $permission ) ||
$this->manager->userCan( $permission, $user, $editpage_Obj->getTitle() );
if ( strpos( $content->getText(), 'wrapclass' ) !== false && !$userCan ) {
throw new PermissionsError( $permission, [ 'templatestylesextender-unscope-no-permisson' ] );
}
return true;
}
}