=== Configuration changes in 1.23 ===
=== New features in 1.23 ===
+* ResourceLoader can utilize the Web Storage API to cache modules client-side.
+ Compared to the browser cache, caching in Web Storage allows ResourceLoader
+ to be more granular about evicting stale modules from the cache while
+ retaining the ability to retrieve multiple modules in a single HTTP request.
+ This capability can be enabled by setting $wgResourceLoaderStorageEnabled to
+ true. This feature is currently considered experimental and should only be
+ enabled with care.
=== Bug fixes in 1.23 ===
* (bug 41759) The "updated since last visit" markers (on history pages, recent
);
class AutoLoader {
+ static $autoloadLocalClassesLower = null;
+
/**
* autoload - take a class name and attempt to load it
*
* as well.
*/
static function autoload( $className ) {
- global $wgAutoloadClasses, $wgAutoloadLocalClasses;
+ global $wgAutoloadClasses, $wgAutoloadLocalClasses,
+ $wgAutoloadAttemptLowercase;
// Workaround for PHP bug <https://bugs.php.net/bug.php?id=49143> (5.3.2. is broken, it's
// fixed in 5.3.6). Strip leading backslashes from class names. When namespaces are used,
$filename = $wgAutoloadLocalClasses[$className];
} elseif ( isset( $wgAutoloadClasses[$className] ) ) {
$filename = $wgAutoloadClasses[$className];
- } else {
- # Try a different capitalisation
- # The case can sometimes be wrong when unserializing PHP 4 objects
+ } elseif ( $wgAutoloadAttemptLowercase ) {
+ /*
+ * Try a different capitalisation.
+ *
+ * PHP 4 objects are always serialized with the classname coerced to lowercase,
+ * and we are plagued with several legacy uses created by MediaWiki < 1.5, see
+ * https://wikitech.wikimedia.org/wiki/Text_storage_data
+ */
$filename = false;
$lowerClass = strtolower( $className );
- foreach ( $wgAutoloadLocalClasses as $class2 => $file2 ) {
- if ( strtolower( $class2 ) == $lowerClass ) {
- $filename = $file2;
- }
+ if ( self::$autoloadLocalClassesLower === null ) {
+ self::$autoloadLocalClassesLower = array_change_key_case( $wgAutoloadLocalClasses, CASE_LOWER );
}
- if ( !$filename ) {
+ if ( isset( self::$autoloadLocalClassesLower[$lowerClass] ) ) {
if ( function_exists( 'wfDebug' ) ) {
- wfDebug( "Class {$className} not found; skipped loading\n" );
+ wfDebug( "Class {$className} was loaded using incorrect case.\n" );
}
+ $filename = self::$autoloadLocalClassesLower[$lowerClass];
+ }
+ }
- # Give up
- return false;
+ if ( !$filename ) {
+ if ( function_exists( 'wfDebug' ) ) {
+ # FIXME: This is not very polite. Assume we do not manage the class.
+ wfDebug( "Class {$className} not found; skipped loading\n" );
}
+
+ # Give up
+ return false;
}
# Make an absolute path, this improves performance by avoiding some stat calls
"$IP/resources/mediawiki.less/",
);
+/**
+ * Whether ResourceLoader should attempt to persist modules in localStorage on
+ * browsers that support the Web Storage API.
+ *
+ * @since 1.23 - Client-side module persistence is experimental. Exercise care.
+ */
+$wgResourceLoaderStorageEnabled = false;
+
+/**
+ * Cache version for client-side ResourceLoader module storage. You can trigger
+ * invalidation of the contents of the module store by incrementing this value.
+ *
+ * @since 1.23
+ */
+$wgResourceLoaderStorageVersion = 1;
+
/** @} */ # End of resource loader settings }
/*************************************************************************//**
*/
$wgAutoloadClasses = array();
+/**
+ * Switch controlling legacy case-insensitive classloading.
+ * Do not disable if your wiki must support data created by PHP4, or by
+ * MediaWiki 1.4 or earlier.
+ */
+$wgAutoloadAttemptLowercase = true;
+
/**
* An array of extension types and inside that their names, versions, authors,
* urls, descriptions and pointers to localized description msgs. Note that
return $fields;
}
+ /**
+ * Return the list of revision fields that should be selected to create
+ * a new revision from an archive row.
+ * @return array
+ */
+ public static function selectArchiveFields() {
+ global $wgContentHandlerUseDB;
+ $fields = array(
+ 'ar_id',
+ 'ar_page_id',
+ 'ar_rev_id',
+ 'ar_text_id',
+ 'ar_timestamp',
+ 'ar_comment',
+ 'ar_user_text',
+ 'ar_user',
+ 'ar_minor_edit',
+ 'ar_deleted',
+ 'ar_len',
+ 'ar_parent_id',
+ 'ar_sha1',
+ );
+
+ if ( $wgContentHandlerUseDB ) {
+ $fields[] = 'ar_content_format';
+ $fields[] = 'ar_content_model';
+ }
+ return $fields;
+ }
+
/**
* Return the list of text fields that should be selected to read the
* revision text
}
$callers = implode( ', ', $callers );
- trigger_error( "DB transaction callbacks still pending (from $callers)." );
+ trigger_error( "DB transaction callbacks still pending (from $callers)." );
}
}
}
// Some of the environment checks make shell requests, remove limits
$GLOBALS['wgMaxShellMemory'] = 0;
+
+ // Don't bother embedding images into generated CSS, which is not cached
+ $GLOBALS['wgResourceLoaderLESSFunctions']['embeddable'] = function( $frame, $less ) {
+ return $less->toBool( false );
+ };
}
/**
/**
* Get the raw vector CSS, flipping if needed
+ *
+ * @todo Possibly get rid of this function and use ResourceLoader in the manner it was
+ * designed to be used in, rather than just grabbing a list of filenames from it,
+ * and not properly handling such details as media types in module definitions.
+ *
* @param string $dir 'ltr' or 'rtl'
* @return String
*/
public function getCSS( $dir ) {
- $skinDir = dirname( dirname( __DIR__ ) ) . '/skins';
-
- // All these files will be concatenated in sequence and loaded
- // as one file.
- // The string 'images/' in the files' contents will be replaced
- // by '../skins/$skinName/images/', where $skinName is what appears
- // before the last '/' in each of the strings.
- $cssFileNames = array(
-
- // Basically the "skins.vector" ResourceLoader module styles
- 'common/shared.css',
- 'common/commonElements.css',
- 'common/commonContent.css',
- 'common/commonInterface.css',
- 'vector/screen.css',
-
- // mw-config specific
- 'common/config.css',
+ // All CSS files these modules reference will be concatenated in sequence
+ // and loaded as one file.
+ $moduleNames = array(
+ 'mediawiki.legacy.shared',
+ 'skins.vector',
+ 'mediawiki.legacy.config',
);
+ $prepend = '';
$css = '';
- wfSuppressWarnings();
- foreach ( $cssFileNames as $cssFileName ) {
- $fullCssFileName = "$skinDir/$cssFileName";
- $cssFileContents = file_get_contents( $fullCssFileName );
- if ( $cssFileContents ) {
- preg_match( "/^(\w+)\//", $cssFileName, $match );
- $skinName = $match[1];
- $css .= str_replace( 'images/', "../skins/$skinName/images/", $cssFileContents );
- } else {
- $css .= "/** Your webserver cannot read $fullCssFileName. Please check file permissions. */";
+ $cssFileNames = array();
+ $resourceLoader = new ResourceLoader();
+ foreach ( $moduleNames as $moduleName ) {
+ $module = $resourceLoader->getModule( $moduleName );
+ $cssFileNames = $module->getAllStyleFiles();
+
+ wfSuppressWarnings();
+ foreach ( $cssFileNames as $cssFileName ) {
+ if ( !file_exists( $cssFileName ) ) {
+ $prepend .= ResourceLoader::makeComment( "Unable to find $cssFileName." );
+ continue;
+ }
+
+ if ( !is_readable( $cssFileName ) ) {
+ $prepend .= ResourceLoader::makeComment( "Unable to read $cssFileName. Please check file permissions." );
+ continue;
+ }
+
+ try {
+
+ if ( preg_match( '/\.less$/', $cssFileName ) ) {
+ // Run the LESS compiler for *.less files (bug 55589)
+ $compiler = ResourceLoader::getLessCompiler();
+ $cssFileContents = $compiler->compileFile( $cssFileName );
+ } else {
+ // Regular CSS file
+ $cssFileContents = file_get_contents( $cssFileName );
+ }
+
+ if ( $cssFileContents ) {
+ // Rewrite URLs, though don't bother embedding images. While static image
+ // files may be cached, CSS returned by this function is definitely not.
+ $cssDirName = dirname( $cssFileName );
+ $css .= CSSMin::remap(
+ /* source */ $cssFileContents,
+ /* local */ $cssDirName,
+ /* remote */ '..' . str_replace(
+ array( $GLOBALS['IP'], DIRECTORY_SEPARATOR ),
+ array( '', '/' ),
+ $cssDirName
+ ),
+ /* embedData */ false
+ );
+ } else {
+ $prepend .= ResourceLoader::makeComment( "Unable to read $cssFileName." );
+ }
+
+ } catch ( Exception $e ) {
+ $prepend .= ResourceLoader::formatException( $e );
+ }
+
+ $css .= "\n";
}
-
- $css .= "\n";
+ wfRestoreWarnings();
}
- wfRestoreWarnings();
+
+ $css = $prepend . $css;
if ( $dir == 'rtl' ) {
$css = CSSJanus::transform( $css, true );
}
/**
- * @todo FIXME: Update documentation. makeLinkObj() is deprecated.
* Replace <!--LINK--> link placeholders with actual links, in the buffer
- * Placeholders created in Skin::makeLinkObj()
+ *
* @return array of link CSS classes, indexed by PDBK.
*/
function replace( &$text ) {
public static function getLessCompiler() {
global $wgResourceLoaderLESSFunctions, $wgResourceLoaderLESSImportPaths;
+ // When called from the installer, it is possible that a required PHP extension
+ // is missing (at least for now; see bug 47564). If this is the case, throw an
+ // exception (caught by the installer) to prevent a fatal error later on.
+ if ( !function_exists( 'ctype_digit' ) ) {
+ throw new MWException( 'lessc requires the Ctype extension' );
+ }
+
$less = new lessc();
$less->setPreserveComments( true );
$less->setVariables( self::getLESSVars() );
$wgVariantArticlePath, $wgActionPaths, $wgVersion,
$wgEnableAPI, $wgEnableWriteAPI, $wgDBname,
$wgSitename, $wgFileExtensions, $wgExtensionAssetsPath,
- $wgCookiePrefix, $wgResourceLoaderMaxQueryLength;
+ $wgCookiePrefix, $wgResourceLoaderMaxQueryLength,
+ $wgResourceLoaderStorageEnabled, $wgResourceLoaderStorageVersion;
$mainPage = Title::newMainPage();
'wgResourceLoaderMaxQueryLength' => $wgResourceLoaderMaxQueryLength,
'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces,
'wgLegalTitleChars' => Title::convertByteClassToUnicodeClass( Title::legalChars() ),
+ 'wgResourceLoaderStorageVersion' => $wgResourceLoaderStorageVersion,
+ 'wgResourceLoaderStorageEnabled' => $wgResourceLoaderStorageEnabled,
);
wfRunHooks( 'ResourceLoaderGetConfigVars', array( &$vars ) );
"mw.Map",
"mw.Message",
"mw.loader",
+ "mw.loader.store",
"mw.log",
"mw.html",
"mw.html.Cdata",
}
$this->output( "Populating rev_len column\n" );
- $rev = $this->doLenUpdates( 'revision', 'rev_id', 'rev' );
+ $rev = $this->doLenUpdates( 'revision', 'rev_id', 'rev', Revision::selectFields() );
$this->output( "Populating ar_len column\n" );
- $ar = $this->doLenUpdates( 'archive', 'ar_id', 'ar' );
+ $ar = $this->doLenUpdates( 'archive', 'ar_id', 'ar', Revision::selectArchiveFields() );
$this->output( "rev_len and ar_len population complete [$rev revision rows, $ar archive rows].\n" );
return true;
}
/**
- * @param $table
- * @param $idCol
- * @param $prefix
+ * @param string $table
+ * @param string $idCol
+ * @param string $prefix
+ * @param array $fields
* @return int
*/
- protected function doLenUpdates( $table, $idCol, $prefix ) {
+ protected function doLenUpdates( $table, $idCol, $prefix, $fields ) {
$db = $this->getDB( DB_MASTER );
$start = $db->selectField( $table, "MIN($idCol)", false, __METHOD__ );
$end = $db->selectField( $table, "MAX($idCol)", false, __METHOD__ );
$blockStart = intval( $start );
$blockEnd = intval( $start ) + $this->mBatchSize - 1;
$count = 0;
- $fields = Revision::selectFields();
+
while ( $blockStart <= $end ) {
- $this->output( "...doing rev_id from $blockStart to $blockEnd\n" );
+ $this->output( "...doing $idCol from $blockStart to $blockEnd\n" );
$res = $db->select(
$table,
$fields,
/**
* @param $row
- * @param $table
- * @param $idCol
- * @param $prefix
+ * @param string $table
+ * @param string $idCol
+ * @param string $prefix
* @return bool
*/
protected function upgradeRow( $row, $table, $idCol, $prefix ) {
'localBasePath' => $GLOBALS['wgStyleDirectory'],
),
'skins.vector' => array(
- // Keep in sync with WebInstallerOutput::getCSS()
+ // Used in the web installer. Test it after modifying this definition!
'styles' => array(
'common/commonElements.css' => array( 'media' => 'screen' ),
'common/commonContent.css' => array( 'media' => 'screen' ),
'localBasePath' => $GLOBALS['wgStyleDirectory'],
),
'mediawiki.legacy.config' => array(
+ // Used in the web installer. Test it after modifying this definition!
'scripts' => 'common/config.js',
- 'styles' => array( 'common/config.css', 'common/config-cc.css' ),
+ 'styles' => array( 'common/config.css' ),
'remoteBasePath' => $GLOBALS['wgStylePath'],
'localBasePath' => $GLOBALS['wgStyleDirectory'],
'dependencies' => 'mediawiki.legacy.wikibits',
'position' => 'top',
),
'mediawiki.legacy.shared' => array(
+ // Used in the web installer. Test it after modifying this definition!
'styles' => array( 'common/shared.css' => array( 'media' => 'screen' ) ),
'remoteBasePath' => $GLOBALS['wgStylePath'],
'localBasePath' => $GLOBALS['wgStyleDirectory'],
}
if ( registry[module].state === 'ready' ) {
- // The current module became 'ready'. Recursively execute all dependent modules that are loaded
- // and now have all dependencies satisfied.
+ // The current module became 'ready'. Set it in the module store, and recursively execute all
+ // dependent modules that are loaded and now have all dependencies satisfied.
+ mw.loader.store.set( module, registry[module] );
for ( m in registry ) {
if ( registry[m].state === 'loaded' && allReady( registry[m].dependencies ) ) {
execute( m );
addScript( sourceLoadScript + '?' + $.param( request ) + '&*', null, async );
}
- /* Public Methods */
+ /* Public Members */
return {
/**
* The module registry is exposed as an aid for debugging and inspecting page
}
}
}
+
+ mw.loader.store.init();
+ if ( mw.loader.store.enabled ) {
+ batch = $.grep( batch, function ( module ) {
+ var source = mw.loader.store.get( module );
+ if ( source ) {
+ $.globalEval( source );
+ return false; // Don't fetch
+ }
+ return true; // Fetch
+ } );
+ }
+
// Early exit if there's nothing to load...
if ( !batch.length ) {
return;
mw.loader.using( 'mediawiki.inspect', function () {
mw.inspect.runReports.apply( mw.inspect, args );
} );
- }
+ },
+
+ /**
+ * On browsers that implement the localStorage API, the module store serves as a
+ * smart complement to the browser cache. Unlike the browser cache, the module store
+ * can slice a concatenated response from ResourceLoader into its constituent
+ * modules and cache each of them separately, using each module's versioning scheme
+ * to determine when the cache should be invalidated.
+ *
+ * @singleton
+ * @class mw.loader.store
+ */
+ store: {
+ // Whether the store is in use on this page.
+ enabled: null,
+ // The contents of the store, mapping '[module name]@[version]' keys
+ // to module implementations.
+ items: {},
+
+ // Cache hit stats
+ stats: { hits: 0, misses: 0, expired: 0 },
+
+ /**
+ * Construct a JSON-serializable object representing the content of the store.
+ * @return {Object} Module store contents.
+ */
+ toJSON: function () {
+ return { items: mw.loader.store.items, vary: mw.loader.store.getVary() };
+ },
+
+ /**
+ * Get the localStorage key for the entire module store. The key references
+ * $wgDBname to prevent clashes between wikis which share a common host.
+ *
+ * @return {string} localStorage item key
+ */
+ getStoreKey: function () {
+ return 'MediaWikiModuleStore:' + mw.config.get( 'wgDBname' );
+ },
+
+ /**
+ * Get a string key on which to vary the module cache.
+ * @return {string} String of concatenated vary conditions.
+ */
+ getVary: function () {
+ return [
+ mw.config.get( 'skin' ),
+ mw.config.get( 'wgResourceLoaderStorageVersion' ),
+ mw.config.get( 'wgUserLanguage' )
+ ].join(':');
+ },
+
+ /**
+ * Get a string key for a specific module. The key format is '[name]@[version]'.
+ *
+ * @param {string} module Module name
+ * @return {string|null} Module key or null if module does not exist
+ */
+ getModuleKey: function ( module ) {
+ return typeof registry[module] === 'object' ?
+ ( module + '@' + registry[module].version ) : null;
+ },
+
+ /**
+ * Initialize the store by retrieving it from localStorage and (if successfully
+ * retrieved) decoding the stored JSON value to a plain object.
+ *
+ * The try / catch block is used for JSON & localStorage feature detection.
+ * See the in-line documentation for Modernizr's localStorage feature detection
+ * code for a full account of why we need a try / catch: <http://git.io/4NEwKg>.
+ */
+ init: function () {
+ var raw, data;
+
+ if ( mw.loader.store.enabled !== null ) {
+ // #init already ran.
+ return;
+ }
+
+ if ( !mw.config.get( 'wgResourceLoaderStorageEnabled' ) || mw.config.get( 'debug' ) ) {
+ // Disabled by configuration, or because debug mode is set.
+ mw.loader.store.enabled = false;
+ return;
+ }
+
+ try {
+ raw = localStorage.getItem( mw.loader.store.getStoreKey() );
+ // If we get here, localStorage is available; mark enabled.
+ mw.loader.store.enabled = true;
+ data = JSON.parse( raw );
+ if ( data && typeof data.items === 'object' && data.vary === mw.loader.store.getVary() ) {
+ mw.loader.store.items = data.items;
+ return;
+ }
+ } catch (e) {}
+
+ if ( raw === undefined ) {
+ mw.loader.store.enabled = false; // localStorage failed; disable store.
+ } else {
+ mw.loader.store.update();
+ }
+ },
+
+ /**
+ * Retrieve a module from the store and update cache hit stats.
+ *
+ * @param {string} module Module name
+ * @return {string|boolean} Module implementation or false if unavailable
+ */
+ get: function ( module ) {
+ var key;
+
+ if ( mw.loader.store.enabled !== true ) {
+ return false;
+ }
+
+ key = mw.loader.store.getModuleKey( module );
+ if ( key in mw.loader.store.items ) {
+ mw.loader.store.stats.hits++;
+ return mw.loader.store.items[key];
+ }
+ mw.loader.store.stats.misses++;
+ return false;
+ },
+
+ /**
+ * Stringify a module and queue it for storage.
+ *
+ * @param {string} module Module name
+ * @param {Object} descriptor The module's descriptor as set in the registry
+ */
+ set: function ( module, descriptor ) {
+ var args, key;
+
+ if ( mw.loader.store.enabled !== true ) {
+ return false;
+ }
+
+ key = mw.loader.store.getModuleKey( module );
+
+ if ( key in mw.loader.store.items ) {
+ // Already set; decline to store.
+ return false;
+ }
+
+ if ( descriptor.state !== 'ready' ) {
+ // Module failed to load; decline to store.
+ return false;
+ }
+
+ if ( !descriptor.version || $.inArray( descriptor.group, [ 'private', 'user', 'site' ] ) !== -1 ) {
+ // Unversioned, private, or site-/user-specific; decline to store.
+ return false;
+ }
+
+ if ( $.inArray( undefined, [ descriptor.script, descriptor.style, descriptor.messages ] ) !== -1 ) {
+ // Partial descriptor; decline to store.
+ return false;
+ }
+
+ try {
+ args = [
+ JSON.stringify( module ),
+ typeof descriptor.script === 'function' ?
+ String( descriptor.script ) : JSON.stringify( descriptor.script ),
+ JSON.stringify( descriptor.style ),
+ JSON.stringify( descriptor.messages )
+ ];
+ } catch (e) {
+ return;
+ }
+ mw.loader.store.items[key] = 'mw.loader.implement(' + args.join(',') + ');';
+ mw.loader.store.update();
+ },
+
+ /**
+ * Iterate through the module store, removing any item that does not correspond
+ * (in name and version) to an item in the module registry.
+ */
+ prune: function () {
+ var key, module;
+
+ if ( mw.loader.store.enabled !== true ) {
+ return false;
+ }
+
+ for ( key in mw.loader.store.items ) {
+ module = key.substring( 0, key.indexOf( '@' ) );
+ if ( mw.loader.store.getModuleKey( module ) !== key ) {
+ mw.loader.store.stats.expired++;
+ delete mw.loader.store.items[key];
+ }
+ }
+ },
+
+ /**
+ * Sync modules to localStorage.
+ *
+ * This function debounces localStorage updates. When called multiple times in
+ * quick succession, the calls are coalesced into a single update operation.
+ * This allows us to call #update without having to consider the module load
+ * queue; the call to localStorage.setItem will be naturally deferred until the
+ * page is quiescent.
+ *
+ * Because localStorage is shared by all pages with the same origin, if multiple
+ * pages are loaded with different module sets, the possibility exists that
+ * modules saved by one page will be clobbered by another. But the impact would
+ * be minor and the problem would be corrected by subsequent page views.
+ */
+ update: ( function () {
+ var timer;
+
+ function flush() {
+ var data;
+ if ( mw.loader.store.enabled !== true ) {
+ return false;
+ }
+ mw.loader.store.prune();
+ try {
+ data = JSON.stringify( mw.loader.store );
+ localStorage.setItem( mw.loader.store.getStoreKey(), data );
+ } catch (e) {}
+ }
+
+ return function () {
+ clearTimeout( timer );
+ timer = setTimeout( flush, 2000 );
+ };
+ }() )
+ }
};
}() ),
--- /dev/null
+<?php
+
+class TestAutoloadedCamlClass {
+}
--- /dev/null
+<?php
+
+class TestAutoloadedClass {
+}
--- /dev/null
+<?php
+
+class TestAutoloadedLocalClass {
+}
--- /dev/null
+<?php
+
+class TestAutoloadedSerializedClass {
+}
<?php
class AutoLoaderTest extends MediaWikiTestCase {
+ protected function setUp() {
+ global $wgAutoloadLocalClasses, $wgAutoloadClasses;
+
+ parent::setUp();
+
+ // Fancy dance to trigger a rebuild of AutoLoader::$autoloadLocalClassesLower
+ $this->testLocalClasses = array(
+ 'TestAutoloadedLocalClass' => __DIR__ . '/../data/autoloader/TestAutoloadedLocalClass.php',
+ 'TestAutoloadedCamlClass' => __DIR__ . '/../data/autoloader/TestAutoloadedCamlClass.php',
+ 'TestAutoloadedSerializedClass' => __DIR__ . '/../data/autoloader/TestAutoloadedSerializedClass.php',
+ );
+ $this->setMwGlobals( 'wgAutoloadLocalClasses', $this->testLocalClasses + $wgAutoloadLocalClasses );
+ InstrumentedAutoLoader::resetAutoloadLocalClassesLower();
+
+ $this->testExtensionClasses = array(
+ 'TestAutoloadedClass' => __DIR__ . '/../data/autoloader/TestAutoloadedClass.php',
+ );
+ $this->setMwGlobals( 'wgAutoloadClasses', $this->testExtensionClasses + $wgAutoloadClasses );
+ }
+
/**
* Assert that there were no classes loaded that are not registered with the AutoLoader.
*
'actual' => $actual,
);
}
+
+ function testCoreClass() {
+ $this->assertTrue( class_exists( 'TestAutoloadedLocalClass' ) );
+ }
+
+ function testExtensionClass() {
+ $this->assertTrue( class_exists( 'TestAutoloadedClass' ) );
+ }
+
+ function testWrongCaseClass() {
+ $this->assertTrue( class_exists( 'testautoLoadedcamlCLASS' ) );
+ }
+
+ function testWrongCaseSerializedClass() {
+ $dummyCereal = 'O:29:"testautoloadedserializedclass":0:{}';
+ $uncerealized = unserialize( $dummyCereal );
+ $this->assertFalse( $uncerealized instanceof __PHP_Incomplete_Class,
+ "unserialize() can load classes case-insensitively.");
+ }
+}
+
+/**
+ * Cheater to poke protected members
+ */
+class InstrumentedAutoLoader extends AutoLoader {
+ static function resetAutoloadLocalClassesLower() {
+ self::$autoloadLocalClassesLower = null;
+ }
}