SHA-1 is not secure enough to be used as a cryptographic hash function, and its implementation in JavaScript is too long and too slow for it to be a good general-purpose hash function. And we currently throw away most of the work: SHA-1 produces 160-bit hash values, of which we keep 48. Although the JavaScript implementation is not exported, SHA-1 is a well-known hash function, and I'm willing to bet that sooner or later someone will move to make it accessible to other modules, at which point usage will start to spread. For ResourceLoader, the qualities we're looking for in a hash function are: * Already implemented in PHP * Easy to implement in JavaScript * Fast * Collision-resistant The requirement that hashes be cheap to compute in JavaScript narrows the field to 32-bit hash functions, because in JavaScript bitwise operators treat their operands as 32 bits, and arithmetic uses double-precision floats, which have a total precision of 53 bits. It's possible to work around these limitations, but it's a lot of extra work. The best match I found is the 32-bit variant of FNV-1, which is available in PHP as of version 5.4 (as 'fnv1a32'). The fnv132 JavaScript function is around ten times faster and eight times shorter than sha1. Change-Id: I1e4fb08d17948538d96f241b2464d594fdc14578
412 lines
9.1 KiB
PHP
412 lines
9.1 KiB
PHP
<?php
|
|
|
|
class ResourceLoaderStartUpModuleTest extends ResourceLoaderTestCase {
|
|
|
|
// Version hash for a blank file module.
|
|
// Result of ResourceLoader::makeHash(), ResourceLoaderTestModule
|
|
// and ResourceLoaderFileModule::getDefinitionSummary().
|
|
protected static $blankVersion = '0a56zyi';
|
|
|
|
protected static function expandPlaceholders( $text ) {
|
|
return strtr( $text, [
|
|
'{blankVer}' => self::$blankVersion
|
|
] );
|
|
}
|
|
|
|
public static function provideGetModuleRegistrations() {
|
|
return [
|
|
[ [
|
|
'msg' => 'Empty registry',
|
|
'modules' => [],
|
|
'out' => '
|
|
mw.loader.addSource( {
|
|
"local": "/w/load.php"
|
|
} );
|
|
mw.loader.register( [] );'
|
|
] ],
|
|
[ [
|
|
'msg' => 'Basic registry',
|
|
'modules' => [
|
|
'test.blank' => new ResourceLoaderTestModule(),
|
|
],
|
|
'out' => '
|
|
mw.loader.addSource( {
|
|
"local": "/w/load.php"
|
|
} );
|
|
mw.loader.register( [
|
|
[
|
|
"test.blank",
|
|
"{blankVer}"
|
|
]
|
|
] );',
|
|
] ],
|
|
[ [
|
|
'msg' => 'Group signature',
|
|
'modules' => [
|
|
'test.blank' => new ResourceLoaderTestModule(),
|
|
'test.group.foo' => new ResourceLoaderTestModule( [ 'group' => 'x-foo' ] ),
|
|
'test.group.bar' => new ResourceLoaderTestModule( [ 'group' => 'x-bar' ] ),
|
|
],
|
|
'out' => '
|
|
mw.loader.addSource( {
|
|
"local": "/w/load.php"
|
|
} );
|
|
mw.loader.register( [
|
|
[
|
|
"test.blank",
|
|
"{blankVer}"
|
|
],
|
|
[
|
|
"test.group.foo",
|
|
"{blankVer}",
|
|
[],
|
|
"x-foo"
|
|
],
|
|
[
|
|
"test.group.bar",
|
|
"{blankVer}",
|
|
[],
|
|
"x-bar"
|
|
]
|
|
] );'
|
|
] ],
|
|
[ [
|
|
'msg' => 'Different target (non-test should not be registered)',
|
|
'modules' => [
|
|
'test.blank' => new ResourceLoaderTestModule(),
|
|
'test.target.foo' => new ResourceLoaderTestModule( [ 'targets' => [ 'x-foo' ] ] ),
|
|
],
|
|
'out' => '
|
|
mw.loader.addSource( {
|
|
"local": "/w/load.php"
|
|
} );
|
|
mw.loader.register( [
|
|
[
|
|
"test.blank",
|
|
"{blankVer}"
|
|
]
|
|
] );'
|
|
] ],
|
|
[ [
|
|
'msg' => 'Foreign source',
|
|
'sources' => [
|
|
'example' => [
|
|
'loadScript' => 'http://example.org/w/load.php',
|
|
'apiScript' => 'http://example.org/w/api.php',
|
|
],
|
|
],
|
|
'modules' => [
|
|
'test.blank' => new ResourceLoaderTestModule( [ 'source' => 'example' ] ),
|
|
],
|
|
'out' => '
|
|
mw.loader.addSource( {
|
|
"local": "/w/load.php",
|
|
"example": "http://example.org/w/load.php"
|
|
} );
|
|
mw.loader.register( [
|
|
[
|
|
"test.blank",
|
|
"{blankVer}",
|
|
[],
|
|
null,
|
|
"example"
|
|
]
|
|
] );'
|
|
] ],
|
|
[ [
|
|
'msg' => 'Conditional dependency function',
|
|
'modules' => [
|
|
'test.x.core' => new ResourceLoaderTestModule(),
|
|
'test.x.polyfill' => new ResourceLoaderTestModule( [
|
|
'skipFunction' => 'return true;'
|
|
] ),
|
|
'test.y.polyfill' => new ResourceLoaderTestModule( [
|
|
'skipFunction' =>
|
|
'return !!(' .
|
|
' window.JSON &&' .
|
|
' JSON.parse &&' .
|
|
' JSON.stringify' .
|
|
');'
|
|
] ),
|
|
'test.z.foo' => new ResourceLoaderTestModule( [
|
|
'dependencies' => [
|
|
'test.x.core',
|
|
'test.x.polyfill',
|
|
'test.y.polyfill',
|
|
],
|
|
] ),
|
|
],
|
|
'out' => '
|
|
mw.loader.addSource( {
|
|
"local": "/w/load.php"
|
|
} );
|
|
mw.loader.register( [
|
|
[
|
|
"test.x.core",
|
|
"{blankVer}"
|
|
],
|
|
[
|
|
"test.x.polyfill",
|
|
"{blankVer}",
|
|
[],
|
|
null,
|
|
null,
|
|
"return true;"
|
|
],
|
|
[
|
|
"test.y.polyfill",
|
|
"{blankVer}",
|
|
[],
|
|
null,
|
|
null,
|
|
"return !!( window.JSON \u0026\u0026 JSON.parse \u0026\u0026 JSON.stringify);"
|
|
],
|
|
[
|
|
"test.z.foo",
|
|
"{blankVer}",
|
|
[
|
|
0,
|
|
1,
|
|
2
|
|
]
|
|
]
|
|
] );',
|
|
] ],
|
|
[ [
|
|
// This may seem like an edge case, but a plain MediaWiki core install
|
|
// with a few extensions installed is likely far more complex than this
|
|
// even, not to mention an install like Wikipedia.
|
|
// TODO: Make this even more realistic.
|
|
'msg' => 'Advanced (everything combined)',
|
|
'sources' => [
|
|
'example' => [
|
|
'loadScript' => 'http://example.org/w/load.php',
|
|
'apiScript' => 'http://example.org/w/api.php',
|
|
],
|
|
],
|
|
'modules' => [
|
|
'test.blank' => new ResourceLoaderTestModule(),
|
|
'test.x.core' => new ResourceLoaderTestModule(),
|
|
'test.x.util' => new ResourceLoaderTestModule( [
|
|
'dependencies' => [
|
|
'test.x.core',
|
|
],
|
|
] ),
|
|
'test.x.foo' => new ResourceLoaderTestModule( [
|
|
'dependencies' => [
|
|
'test.x.core',
|
|
],
|
|
] ),
|
|
'test.x.bar' => new ResourceLoaderTestModule( [
|
|
'dependencies' => [
|
|
'test.x.core',
|
|
'test.x.util',
|
|
],
|
|
] ),
|
|
'test.x.quux' => new ResourceLoaderTestModule( [
|
|
'dependencies' => [
|
|
'test.x.foo',
|
|
'test.x.bar',
|
|
'test.x.util',
|
|
'test.x.unknown',
|
|
],
|
|
] ),
|
|
'test.group.foo.1' => new ResourceLoaderTestModule( [
|
|
'group' => 'x-foo',
|
|
] ),
|
|
'test.group.foo.2' => new ResourceLoaderTestModule( [
|
|
'group' => 'x-foo',
|
|
] ),
|
|
'test.group.bar.1' => new ResourceLoaderTestModule( [
|
|
'group' => 'x-bar',
|
|
] ),
|
|
'test.group.bar.2' => new ResourceLoaderTestModule( [
|
|
'group' => 'x-bar',
|
|
'source' => 'example',
|
|
] ),
|
|
'test.target.foo' => new ResourceLoaderTestModule( [
|
|
'targets' => [ 'x-foo' ],
|
|
] ),
|
|
'test.target.bar' => new ResourceLoaderTestModule( [
|
|
'source' => 'example',
|
|
'targets' => [ 'x-foo' ],
|
|
] ),
|
|
],
|
|
'out' => '
|
|
mw.loader.addSource( {
|
|
"local": "/w/load.php",
|
|
"example": "http://example.org/w/load.php"
|
|
} );
|
|
mw.loader.register( [
|
|
[
|
|
"test.blank",
|
|
"{blankVer}"
|
|
],
|
|
[
|
|
"test.x.core",
|
|
"{blankVer}"
|
|
],
|
|
[
|
|
"test.x.util",
|
|
"{blankVer}",
|
|
[
|
|
1
|
|
]
|
|
],
|
|
[
|
|
"test.x.foo",
|
|
"{blankVer}",
|
|
[
|
|
1
|
|
]
|
|
],
|
|
[
|
|
"test.x.bar",
|
|
"{blankVer}",
|
|
[
|
|
2
|
|
]
|
|
],
|
|
[
|
|
"test.x.quux",
|
|
"{blankVer}",
|
|
[
|
|
3,
|
|
4,
|
|
"test.x.unknown"
|
|
]
|
|
],
|
|
[
|
|
"test.group.foo.1",
|
|
"{blankVer}",
|
|
[],
|
|
"x-foo"
|
|
],
|
|
[
|
|
"test.group.foo.2",
|
|
"{blankVer}",
|
|
[],
|
|
"x-foo"
|
|
],
|
|
[
|
|
"test.group.bar.1",
|
|
"{blankVer}",
|
|
[],
|
|
"x-bar"
|
|
],
|
|
[
|
|
"test.group.bar.2",
|
|
"{blankVer}",
|
|
[],
|
|
"x-bar",
|
|
"example"
|
|
]
|
|
] );'
|
|
] ],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideGetModuleRegistrations
|
|
* @covers ResourceLoaderStartUpModule::compileUnresolvedDependencies
|
|
* @covers ResourceLoaderStartUpModule::getModuleRegistrations
|
|
* @covers ResourceLoader::makeLoaderSourcesScript
|
|
* @covers ResourceLoader::makeLoaderRegisterScript
|
|
*/
|
|
public function testGetModuleRegistrations( $case ) {
|
|
if ( isset( $case['sources'] ) ) {
|
|
$this->setMwGlobals( 'wgResourceLoaderSources', $case['sources'] );
|
|
}
|
|
|
|
$context = $this->getResourceLoaderContext();
|
|
$rl = $context->getResourceLoader();
|
|
$rl->register( $case['modules'] );
|
|
$module = new ResourceLoaderStartUpModule();
|
|
$out = ltrim( $case['out'], "\n" );
|
|
|
|
$this->assertEquals(
|
|
self::expandPlaceholders( $out ),
|
|
$module->getModuleRegistrations( $context ),
|
|
$case['msg']
|
|
);
|
|
}
|
|
|
|
public static function provideRegistrations() {
|
|
return [
|
|
[ [
|
|
'test.blank' => new ResourceLoaderTestModule(),
|
|
'test.min' => new ResourceLoaderTestModule( [
|
|
'skipFunction' =>
|
|
'return !!(' .
|
|
' window.JSON &&' .
|
|
' JSON.parse &&' .
|
|
' JSON.stringify' .
|
|
');',
|
|
'dependencies' => [
|
|
'test.blank',
|
|
],
|
|
] ),
|
|
] ]
|
|
];
|
|
}
|
|
/**
|
|
* @dataProvider provideRegistrations
|
|
*/
|
|
public function testRegistrationsMinified( $modules ) {
|
|
$this->setMwGlobals( 'wgResourceLoaderDebug', false );
|
|
|
|
$context = $this->getResourceLoaderContext();
|
|
$rl = $context->getResourceLoader();
|
|
$rl->register( $modules );
|
|
$module = new ResourceLoaderStartUpModule();
|
|
$out = 'mw.loader.addSource({"local":"/w/load.php"});' . "\n"
|
|
. 'mw.loader.register(['
|
|
. '["test.blank","{blankVer}"],'
|
|
. '["test.min","{blankVer}",[0],null,null,'
|
|
. '"return!!(window.JSON\u0026\u0026JSON.parse\u0026\u0026JSON.stringify);"'
|
|
. ']]);';
|
|
|
|
$this->assertEquals(
|
|
self::expandPlaceholders( $out ),
|
|
$module->getModuleRegistrations( $context ),
|
|
'Minified output'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideRegistrations
|
|
*/
|
|
public function testRegistrationsUnminified( $modules ) {
|
|
$context = $this->getResourceLoaderContext();
|
|
$rl = $context->getResourceLoader();
|
|
$rl->register( $modules );
|
|
$module = new ResourceLoaderStartUpModule();
|
|
$out =
|
|
'mw.loader.addSource( {
|
|
"local": "/w/load.php"
|
|
} );
|
|
mw.loader.register( [
|
|
[
|
|
"test.blank",
|
|
"{blankVer}"
|
|
],
|
|
[
|
|
"test.min",
|
|
"{blankVer}",
|
|
[
|
|
0
|
|
],
|
|
null,
|
|
null,
|
|
"return !!( window.JSON \u0026\u0026 JSON.parse \u0026\u0026 JSON.stringify);"
|
|
]
|
|
] );';
|
|
|
|
$this->assertEquals(
|
|
self::expandPlaceholders( $out ),
|
|
$module->getModuleRegistrations( $context ),
|
|
'Unminified output'
|
|
);
|
|
}
|
|
|
|
}
|