From 7f51cf22f918089a0c9669a60c9c3fb92bbd16b8 Mon Sep 17 00:00:00 2001 From: Adam Roses Wight Date: Sat, 26 Oct 2013 22:59:01 -0700 Subject: [PATCH] Minor optimization to the AutoLoader When MediaWiki autoloading fails, we should gracefully return false. Instead, we have been calling strtolower roughly 1,000 times in the hope of finding a case-insensitive match. This patch preserves the legacy case-insensitivity, but improves its performance by approximately 100x, by storing the case-insensitive class lookups as a static variable. There is a new global $wgAutoloadAttemptLowercase which will switch the behavior if desired. The default is to support case-insensitive loading. Change-Id: Ifb12e05614a48390b730167e9d4ddcd8545db764 --- includes/AutoLoader.php | 38 ++++++++++----- includes/DefaultSettings.php | 7 +++ .../autoloader/TestAutoloadedCamlClass.php | 4 ++ .../data/autoloader/TestAutoloadedClass.php | 4 ++ .../autoloader/TestAutoloadedLocalClass.php | 4 ++ .../TestAutoloadedSerializedClass.php | 4 ++ tests/phpunit/structure/AutoLoaderTest.php | 48 +++++++++++++++++++ 7 files changed, 97 insertions(+), 12 deletions(-) create mode 100644 tests/phpunit/data/autoloader/TestAutoloadedCamlClass.php create mode 100644 tests/phpunit/data/autoloader/TestAutoloadedClass.php create mode 100644 tests/phpunit/data/autoloader/TestAutoloadedLocalClass.php create mode 100644 tests/phpunit/data/autoloader/TestAutoloadedSerializedClass.php diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 0706fe3f5c..dbba50007c 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -1136,6 +1136,8 @@ $wgAutoloadLocalClasses = array( ); class AutoLoader { + static $autoloadLocalClassesLower = null; + /** * autoload - take a class name and attempt to load it * @@ -1145,7 +1147,8 @@ class AutoLoader { * as well. */ static function autoload( $className ) { - global $wgAutoloadClasses, $wgAutoloadLocalClasses; + global $wgAutoloadClasses, $wgAutoloadLocalClasses, + $wgAutoloadAttemptLowercase; // Workaround for PHP bug (5.3.2. is broken, it's // fixed in 5.3.6). Strip leading backslashes from class names. When namespaces are used, @@ -1160,26 +1163,37 @@ class AutoLoader { $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 diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index dbdd89e52e..bf2d2fdf02 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -5953,6 +5953,13 @@ $wgSpecialPages = array(); */ $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 diff --git a/tests/phpunit/data/autoloader/TestAutoloadedCamlClass.php b/tests/phpunit/data/autoloader/TestAutoloadedCamlClass.php new file mode 100644 index 0000000000..6dfce7a1b3 --- /dev/null +++ b/tests/phpunit/data/autoloader/TestAutoloadedCamlClass.php @@ -0,0 +1,4 @@ +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. * @@ -53,4 +73,32 @@ class AutoLoaderTest extends MediaWikiTestCase { '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; + } } -- 2.20.1