Follow-up r64670 (bug22929): cleaner implementation of security for script (and potentially CSS) files. ResourceLoader *already* knows where each module has come from, so all we need to do is filter them in OutputPage according to the desired level of 'trustworthiness'.
TODO: * Are there instances where we might want to restrict CSS as well as JS? * Would a $wg config option and/or user preference and/or index.php GET parameter to limit inclusion be useful? * Can we deprecate any of the existing $wg config options? * What's going on with the duplicated code between OutputPage and SkinTemplate?
This commit is contained in:
parent
47529179e9
commit
da36f65433
6 changed files with 163 additions and 40 deletions
|
|
@ -124,9 +124,12 @@ class OutputPage {
|
|||
|
||||
var $mTemplateIds = array();
|
||||
|
||||
/** Initialized with a global value. Let us override it.
|
||||
* Should probably get deleted / rewritten ... */
|
||||
var $mAllowUserJs;
|
||||
# What level of 'untrustworthiness' is allowed in CSS/JS modules loaded on this page?
|
||||
# @see ResourceLoaderModule::$origin
|
||||
# ResourceLoaderModule::ORIGIN_ALL is assumed unless overridden;
|
||||
protected $mAllowedModules = array(
|
||||
ResourceLoaderModule::TYPE_COMBINED => ResourceLoaderModule::ORIGIN_ALL,
|
||||
);
|
||||
|
||||
/**
|
||||
* @EasterEgg I just love the name for this self documenting variable.
|
||||
|
|
@ -211,15 +214,6 @@ class OutputPage {
|
|||
'Cookie' => null
|
||||
);
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* Initialise private variables
|
||||
*/
|
||||
function __construct() {
|
||||
global $wgAllowUserJs;
|
||||
$this->mAllowUserJs = $wgAllowUserJs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to $url rather than displaying the normal page
|
||||
*
|
||||
|
|
@ -366,13 +360,34 @@ class OutputPage {
|
|||
return $this->mScripts . $this->getHeadItems();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter an array of modules to remove insufficiently trustworthy members
|
||||
* @param $modules Array
|
||||
* @return Array
|
||||
*/
|
||||
protected function filterModules( $modules, $type = ResourceLoaderModule::TYPE_COMBINED ){
|
||||
$resourceLoader = $this->getResourceLoader();
|
||||
$filteredModules = array();
|
||||
foreach( $modules as $val ){
|
||||
$module = $resourceLoader->getModule( $val );
|
||||
if( $module->getOrigin() <= $this->getAllowedModules( $type ) ) {
|
||||
$filteredModules[] = $val;
|
||||
}
|
||||
}
|
||||
return $filteredModules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of modules to include on this page
|
||||
*
|
||||
* @param $filter Bool whether to filter out insufficiently trustworthy modules
|
||||
* @return Array of module names
|
||||
*/
|
||||
public function getModules() {
|
||||
return array_values( array_unique( $this->mModules ) );
|
||||
public function getModules( $filter = false, $param = 'mModules' ) {
|
||||
$modules = array_values( array_unique( $this->$param ) );
|
||||
return $filter
|
||||
? $this->filterModules( $modules )
|
||||
: $modules;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -390,8 +405,8 @@ class OutputPage {
|
|||
* Get the list of module JS to include on this page
|
||||
* @return array of module names
|
||||
*/
|
||||
public function getModuleScripts() {
|
||||
return array_values( array_unique( $this->mModuleScripts ) );
|
||||
public function getModuleScripts( $filter = false ) {
|
||||
return $this->getModules( $filter, 'mModuleScripts' );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -410,8 +425,8 @@ class OutputPage {
|
|||
*
|
||||
* @return Array of module names
|
||||
*/
|
||||
public function getModuleStyles() {
|
||||
return array_values( array_unique( $this->mModuleStyles ) );
|
||||
public function getModuleStyles( $filter = false ) {
|
||||
return $this->getModules( $filter, 'mModuleStyles' );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -430,8 +445,8 @@ class OutputPage {
|
|||
*
|
||||
* @return Array of module names
|
||||
*/
|
||||
public function getModuleMessages() {
|
||||
return array_values( array_unique( $this->mModuleMessages ) );
|
||||
public function getModuleMessages( $filter = false ) {
|
||||
return $this->getModules( $filter, 'mModuleMessages' );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1065,19 +1080,58 @@ class OutputPage {
|
|||
}
|
||||
|
||||
/**
|
||||
* Remove user JavaScript from scripts to load
|
||||
* Do not allow scripts which can be modified by wiki users to load on this page;
|
||||
* only allow scripts bundled with, or generated by, the software.
|
||||
*/
|
||||
public function disallowUserJs() {
|
||||
$this->mAllowUserJs = false;
|
||||
$this->reduceAllowedModules(
|
||||
ResourceLoaderModule::TYPE_SCRIPTS,
|
||||
ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether user JavaScript is allowed for this page
|
||||
*
|
||||
* @deprecated @since 1.18 Load modules with ResourceLoader, and origin and
|
||||
* trustworthiness is identified and enforced automagically.
|
||||
* @return Boolean
|
||||
*/
|
||||
public function isUserJsAllowed() {
|
||||
return $this->mAllowUserJs;
|
||||
return $this->getAllowedModules( ResourceLoaderModule::TYPE_SCRIPTS ) >= ResourceLoaderModule::ORIGIN_USER_INDIVIDUAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show what level of JavaScript / CSS untrustworthiness is allowed on this page
|
||||
* @see ResourceLoaderModule::$origin
|
||||
* @param $type String ResourceLoaderModule TYPE_ constant
|
||||
* @return Int ResourceLoaderModule ORIGIN_ class constant
|
||||
*/
|
||||
public function getAllowedModules( $type ){
|
||||
if( $type == ResourceLoaderModule::TYPE_COMBINED ){
|
||||
return min( array_values( $this->mAllowedModules ) );
|
||||
} else {
|
||||
return isset( $this->mAllowedModules[$type] )
|
||||
? $this->mAllowedModules[$type]
|
||||
: ResourceLoaderModule::ORIGIN_ALL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the highest level of CSS/JS untrustworthiness allowed
|
||||
* @param $type String ResourceLoaderModule TYPE_ constant
|
||||
* @param $level Int ResourceLoaderModule class constant
|
||||
*/
|
||||
public function setAllowedModules( $type, $level ){
|
||||
$this->mAllowedModules[$type] = $level;
|
||||
}
|
||||
|
||||
/**
|
||||
* As for setAllowedModules(), but don't inadvertantly make the page more accessible
|
||||
* @param $type String
|
||||
* @param $level Int ResourceLoaderModule class constant
|
||||
*/
|
||||
public function reduceAllowedModules( $type, $level ){
|
||||
$this->mAllowedModules[$type] = min( $this->getAllowedModules($type), $level );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -2347,7 +2401,7 @@ class OutputPage {
|
|||
* TODO: Document
|
||||
* @param $skin Skin
|
||||
* @param $modules Array/string with the module name
|
||||
* @param $only string May be styles, messages or scripts
|
||||
* @param $only String ResourceLoaderModule TYPE_ class constant
|
||||
* @param $useESI boolean
|
||||
* @return string html <script> and <style> tags
|
||||
*/
|
||||
|
|
@ -2396,12 +2450,23 @@ class OutputPage {
|
|||
$resourceLoader = $this->getResourceLoader();
|
||||
foreach ( (array) $modules as $name ) {
|
||||
$module = $resourceLoader->getModule( $name );
|
||||
# Check that we're allowed to include this module on this page
|
||||
if( ( $module->getOrigin() > $this->getAllowedModules( ResourceLoaderModule::TYPE_SCRIPTS )
|
||||
&& $only == ResourceLoaderModule::TYPE_SCRIPTS )
|
||||
|| ( $module->getOrigin() > $this->getAllowedModules( ResourceLoaderModule::TYPE_STYLES )
|
||||
&& $only == ResourceLoaderModule::TYPE_STYLES )
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$group = $module->getGroup();
|
||||
if ( !isset( $groups[$group] ) ) {
|
||||
$groups[$group] = array();
|
||||
}
|
||||
$groups[$group][$name] = $module;
|
||||
}
|
||||
|
||||
$links = '';
|
||||
foreach ( $groups as $group => $modules ) {
|
||||
$query['modules'] = implode( '|', array_keys( $modules ) );
|
||||
|
|
@ -2412,7 +2477,7 @@ class OutputPage {
|
|||
// Support inlining of private modules if configured as such
|
||||
if ( $group === 'private' && $wgResourceLoaderInlinePrivateModules ) {
|
||||
$context = new ResourceLoaderContext( $resourceLoader, new FauxRequest( $query ) );
|
||||
if ( $only == 'styles' ) {
|
||||
if ( $only == ResourceLoaderModule::TYPE_STYLES ) {
|
||||
$links .= Html::inlineStyle(
|
||||
$resourceLoader->makeModuleResponse( $context, $modules )
|
||||
);
|
||||
|
|
@ -2446,14 +2511,14 @@ class OutputPage {
|
|||
$url = wfAppendQuery( $wgLoadScript, $query );
|
||||
if ( $useESI && $wgResourceLoaderUseESI ) {
|
||||
$esi = Xml::element( 'esi:include', array( 'src' => $url ) );
|
||||
if ( $only == 'styles' ) {
|
||||
if ( $only == ResourceLoaderModule::TYPE_STYLES ) {
|
||||
$links .= Html::inlineStyle( $esi );
|
||||
} else {
|
||||
$links .= Html::inlineScript( $esi );
|
||||
}
|
||||
} else {
|
||||
// Automatically select style/script elements
|
||||
if ( $only === 'styles' ) {
|
||||
if ( $only === ResourceLoaderModule::TYPE_STYLES ) {
|
||||
$links .= Html::linkedStyle( wfAppendQuery( $wgLoadScript, $query ) ) . "\n";
|
||||
} else {
|
||||
$links .= Html::linkedScript( wfAppendQuery( $wgLoadScript, $query ) ) . "\n";
|
||||
|
|
@ -2472,24 +2537,24 @@ class OutputPage {
|
|||
* @return String: HTML fragment
|
||||
*/
|
||||
function getHeadScripts( Skin $sk ) {
|
||||
global $wgUser, $wgRequest, $wgUseSiteJs;
|
||||
global $wgUser, $wgRequest, $wgUseSiteJs, $wgAllowUserJs;
|
||||
|
||||
// Startup - this will immediately load jquery and mediawiki modules
|
||||
$scripts = $this->makeResourceLoaderLink( $sk, 'startup', 'scripts', true );
|
||||
$scripts = $this->makeResourceLoaderLink( $sk, 'startup', ResourceLoaderModule::TYPE_SCRIPTS, true );
|
||||
|
||||
// Configuration -- This could be merged together with the load and go, but
|
||||
// makeGlobalVariablesScript returns a whole script tag -- grumble grumble...
|
||||
$scripts .= Skin::makeGlobalVariablesScript( $sk->getSkinName() ) . "\n";
|
||||
|
||||
// Script and Messages "only" requests
|
||||
$scripts .= $this->makeResourceLoaderLink( $sk, $this->getModuleScripts(), 'scripts' );
|
||||
$scripts .= $this->makeResourceLoaderLink( $sk, $this->getModuleMessages(), 'messages' );
|
||||
$scripts .= $this->makeResourceLoaderLink( $sk, $this->getModuleScripts( true ), ResourceLoaderModule::TYPE_SCRIPTS );
|
||||
$scripts .= $this->makeResourceLoaderLink( $sk, $this->getModuleMessages( true ), ResourceLoaderModule::TYPE_MESSAGES );
|
||||
|
||||
// Modules requests - let the client calculate dependencies and batch requests as it likes
|
||||
if ( $this->getModules() ) {
|
||||
if ( $this->getModules( true ) ) {
|
||||
$scripts .= Html::inlineScript(
|
||||
ResourceLoader::makeLoaderConditionalScript(
|
||||
Xml::encodeJsCall( 'mediaWiki.loader.load', array( $this->getModules() ) ) .
|
||||
Xml::encodeJsCall( 'mediaWiki.loader.load', array( $this->getModules( true ) ) ) .
|
||||
Xml::encodeJsCall( 'mediaWiki.loader.go', array() )
|
||||
)
|
||||
) . "\n";
|
||||
|
|
@ -2500,25 +2565,25 @@ class OutputPage {
|
|||
|
||||
// Add site JS if enabled
|
||||
if ( $wgUseSiteJs ) {
|
||||
$scripts .= $this->makeResourceLoaderLink( $sk, 'site', 'scripts' );
|
||||
$scripts .= $this->makeResourceLoaderLink( $sk, 'site', ResourceLoaderModule::TYPE_SCRIPTS );
|
||||
}
|
||||
|
||||
// Add user JS if enabled - trying to load user.options as a bundle if possible
|
||||
$userOptionsAdded = false;
|
||||
if ( $this->isUserJsAllowed() && $wgUser->isLoggedIn() ) {
|
||||
if ( $wgAllowUserJs && $wgUser->isLoggedIn() ) {
|
||||
$action = $wgRequest->getVal( 'action', 'view' );
|
||||
if( $this->mTitle && $this->mTitle->isJsSubpage() && $sk->userCanPreview( $action ) ) {
|
||||
# XXX: additional security check/prompt?
|
||||
$scripts .= Html::inlineScript( "\n" . $wgRequest->getText( 'wpTextbox1' ) . "\n" ) . "\n";
|
||||
} else {
|
||||
$scripts .= $this->makeResourceLoaderLink(
|
||||
$sk, array( 'user', 'user.options' ), 'scripts'
|
||||
$sk, array( 'user', 'user.options' ), ResourceLoaderModule::TYPE_SCRIPTS
|
||||
);
|
||||
$userOptionsAdded = true;
|
||||
}
|
||||
}
|
||||
if ( !$userOptionsAdded ) {
|
||||
$scripts .= $this->makeResourceLoaderLink( $sk, 'user.options', 'scripts' );
|
||||
$scripts .= $this->makeResourceLoaderLink( $sk, 'user.options', ResourceLoaderModule::TYPE_SCRIPTS );
|
||||
}
|
||||
|
||||
return $scripts;
|
||||
|
|
@ -2713,7 +2778,7 @@ class OutputPage {
|
|||
// dynamically added styles to override statically added styles from other modules. So the order
|
||||
// has to be other, dynamic, site, user
|
||||
// Add statically added styles for other modules
|
||||
$ret .= $this->makeResourceLoaderLink( $sk, $styles['other'], 'styles' );
|
||||
$ret .= $this->makeResourceLoaderLink( $sk, $styles['other'], ResourceLoaderModule::TYPE_STYLES );
|
||||
// Add normal styles added through addStyle()/addInlineStyle() here
|
||||
$ret .= implode( "\n", $this->buildCssLinksArray() ) . $this->mInlineStyles;
|
||||
// Add marker tag to mark the place where the client-side loader should inject dynamic styles
|
||||
|
|
@ -2721,7 +2786,7 @@ class OutputPage {
|
|||
$ret .= Html::element( 'meta', array( 'name' => 'ResourceLoaderDynamicStyles', 'content' => '' ) );
|
||||
// Add site and user styles
|
||||
$ret .= $this->makeResourceLoaderLink(
|
||||
$sk, array_merge( $styles['site'], $styles['user'] ), 'styles'
|
||||
$sk, array_merge( $styles['site'], $styles['user'] ), ResourceLoaderModule::TYPE_STYLES
|
||||
);
|
||||
return $ret;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -201,6 +201,8 @@ class SkinTemplate extends Skin {
|
|||
$tpl->setRef( 'usercss', $this->usercss );
|
||||
|
||||
$this->userjs = $this->userjsprev = false;
|
||||
# FIXME: this is the only use of OutputPage::isUserJsAllowed() anywhere; can we
|
||||
# get rid of it? For that matter, why is any of this here at all?
|
||||
$this->setupUserJs( $out->isUserJsAllowed() );
|
||||
$tpl->setRef( 'userjs', $this->userjs );
|
||||
$tpl->setRef( 'userjsprev', $this->userjsprev );
|
||||
|
|
@ -1255,6 +1257,7 @@ class SkinTemplate extends Skin {
|
|||
|
||||
/**
|
||||
* @private
|
||||
* FIXME: why is this duplicated in/from OutputPage::getHeadScripts()??
|
||||
*/
|
||||
function setupUserJs( $allowUserJs ) {
|
||||
global $wgRequest, $wgJsMimeType;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,34 @@
|
|||
* Abstraction for resource loader modules, with name registration and maxage functionality.
|
||||
*/
|
||||
abstract class ResourceLoaderModule {
|
||||
|
||||
# Type of resource
|
||||
const TYPE_SCRIPTS = 'scripts';
|
||||
const TYPE_STYLES = 'styles';
|
||||
const TYPE_MESSAGES = 'messages';
|
||||
const TYPE_COMBINED = 'combined';
|
||||
|
||||
# sitewide core module like a skin file or jQuery component
|
||||
const ORIGIN_CORE_SITEWIDE = 1;
|
||||
|
||||
# per-user module generated by the software
|
||||
const ORIGIN_CORE_INDIVIDUAL = 2;
|
||||
|
||||
# sitewide module generated from user-editable files, like MediaWiki:Common.js, or
|
||||
# modules accessible to multiple users, such as those generated by the Gadgets extension.
|
||||
const ORIGIN_USER_SITEWIDE = 3;
|
||||
|
||||
# per-user module generated from user-editable files, like User:Me/Monobook.js
|
||||
const ORIGIN_USER_INDIVIDUAL = 4;
|
||||
|
||||
# an access constant; make sure this is kept as the largest number in this group
|
||||
const ORIGIN_ALL = 10;
|
||||
|
||||
# script and style modules form a hierarchy of trustworthiness, with core modules like
|
||||
# skins and jQuery as most trustworthy, and user scripts as least trustworthy. We can
|
||||
# limit the types of scripts and styles we allow to load on, say, sensitive special
|
||||
# pages like Special:UserLogin and Special:Preferences
|
||||
protected $origin = self::ORIGIN_CORE_SITEWIDE;
|
||||
|
||||
/* Protected Members */
|
||||
|
||||
|
|
@ -56,6 +84,27 @@ abstract class ResourceLoaderModule {
|
|||
$this->name = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this module's origin. This is set when the module is registered
|
||||
* with ResourceLoader::register()
|
||||
*
|
||||
* @return Int ResourceLoaderModule class constant, the subclass default
|
||||
* if not set manuall
|
||||
*/
|
||||
public function getOrigin() {
|
||||
return $this->origin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this module's origin. This is called by ResourceLodaer::register()
|
||||
* when registering the module. Other code should not call this.
|
||||
*
|
||||
* @param $name Int origin
|
||||
*/
|
||||
public function setOrigin( $origin ) {
|
||||
$this->origin = $origin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether CSS for this module should be flipped
|
||||
* @param $context ResourceLoaderContext
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
class ResourceLoaderUserModule extends ResourceLoaderWikiModule {
|
||||
|
||||
/* Protected Methods */
|
||||
protected $origin = self::ORIGIN_USER_INDIVIDUAL;
|
||||
|
||||
protected function getPages( ResourceLoaderContext $context ) {
|
||||
if ( $context->getUser() ) {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ class ResourceLoaderUserOptionsModule extends ResourceLoaderModule {
|
|||
|
||||
protected $modifiedTime = array();
|
||||
|
||||
protected $origin = self::ORIGIN_CORE_INDIVIDUAL;
|
||||
|
||||
/* Methods */
|
||||
|
||||
public function getModifiedTime( ResourceLoaderContext $context ) {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ defined( 'MEDIAWIKI' ) || die( 1 );
|
|||
abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
|
||||
|
||||
/* Protected Members */
|
||||
|
||||
# Origin is user-supplied code
|
||||
protected $origin = self::ORIGIN_USER_SITEWIDE;
|
||||
|
||||
// In-object cache for modified time
|
||||
protected $modifiedTime = array();
|
||||
|
|
|
|||
Loading…
Reference in a new issue