From 550b878e63cd03c225194370337ee2f876626894 Mon Sep 17 00:00:00 2001 From: nullspoon Date: Tue, 7 Aug 2012 16:06:12 -0600 Subject: [PATCH] Whitelisting publicly readable title with regex This patch make it possible to whitelist pages which anonymous users may see. It is similar to $wgWhitelistRead expect it uses regular expressions, the list of regex are to be added in the new global array $wgWhitelistReadRegexp. This would be useful in a semi-public team wiki situation where the admin would want to hide an entire namespace from everyone except those in a particular group due to sensitive team specific information. Added new unit test testUserCan in includes/TitleTest.php to test this new functionality. * adds $wgWhitelistReadRegexp to DefaultSettings.php * updates RELEASE-NOTES-1.21 new features * updates CREDITS Signed-off-by: Antoine Musso Change-Id: I83f6a614874c3d289ff4bd8d015f1d9c92e500b6 --- CREDITS | 1 + RELEASE-NOTES-1.21 | 1 + includes/DefaultSettings.php | 28 +++++++ includes/Title.php | 13 ++- tests/phpunit/includes/TitleTest.php | 116 +++++++++++++++++++++++++++ 5 files changed, 158 insertions(+), 1 deletion(-) diff --git a/CREDITS b/CREDITS index a03ad5ebf7..7da6cb6532 100644 --- a/CREDITS +++ b/CREDITS @@ -87,6 +87,7 @@ following names for their contribution to the product. == Patch Contributors == * Aaron Pramana +* Aaron Ball * Agbad * Ahmad Sherif * Alejandro Mery diff --git a/RELEASE-NOTES-1.21 b/RELEASE-NOTES-1.21 index 1efd80d425..b9fdecfbeb 100644 --- a/RELEASE-NOTES-1.21 +++ b/RELEASE-NOTES-1.21 @@ -85,6 +85,7 @@ production. a security fix (bug 42202). * Added the ability to limit the wall clock time used by shell processes, as well as the CPU time. Configurable with $wgMaxShellWallClockTime. +* Added $wgWhitelistReadRegexp for regex whitelisting === Bug fixes in 1.21 === * (bug 40353) SpecialDoubleRedirect should support interwiki redirects. diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index d68d9915f1..6a4f6a60a3 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -3816,6 +3816,34 @@ $wgBlockDisablesLogin = false; */ $wgWhitelistRead = false; +/** + * Pages anonymous user may see, set as an array of regular expressions. + * + * This function will match the regexp against the title name, which + * is without underscore. + * + * @par Example: + * To whitelist [[Main Page]]: + * @code + * $wgWhitelistReadRegexp = array( "/Main Page/" ); + * @endcode + * + * @note Unless ^ and/or $ is specified, a regular expression might match + * pages not intended to be whitelisted. The above example will also + * whitelist a page named 'Security Main Page'. + * + * @par Example: + * To allow reading any page starting with 'User' regardless of the case: + * @code + * $wgWhitelistReadRegexp = array( "@^UsEr.*@i" ); + * @endcode + * Will allow both [[User is banned]] and [[User:JohnDoe]] + * + * @note This will only work if $wgGroupPermissions['*']['read'] is false -- + * see below. Otherwise, ALL pages are accessible, regardless of this setting. + */ +$wgWhitelistReadRegexp = false; + /** * Should editors be required to have a validated e-mail * address before being allowed to edit? diff --git a/includes/Title.php b/includes/Title.php index 9de94d8497..a3f17a7fea 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -2087,7 +2087,7 @@ class Title { * @return Array list of errors */ private function checkReadPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) { - global $wgWhitelistRead, $wgRevokePermissions; + global $wgWhitelistRead, $wgWhitelistReadRegexp, $wgRevokePermissions; static $useShortcut = null; # Initialize the $useShortcut boolean, to determine if we can skip quite a bit of code below @@ -2155,6 +2155,17 @@ class Title { } } + if( !$whitelisted && is_array( $wgWhitelistReadRegexp ) && !empty( $wgWhitelistReadRegexp ) ) { + $name = $this->getPrefixedText(); + // Check for regex whitelisting + foreach ( $wgWhitelistReadRegexp as $listItem ) { + if ( preg_match( $listItem, $name ) ) { + $whitelisted = true; + break; + } + } + } + if ( !$whitelisted ) { # If the title is not whitelisted, give extensions a chance to do so... wfRunHooks( 'TitleReadWhitelist', array( $this, $user, &$whitelisted ) ); diff --git a/tests/phpunit/includes/TitleTest.php b/tests/phpunit/includes/TitleTest.php index 86479540d8..6399d48378 100644 --- a/tests/phpunit/includes/TitleTest.php +++ b/tests/phpunit/includes/TitleTest.php @@ -79,6 +79,122 @@ class TitleTest extends MediaWikiTestCase { } } + /** + * Provides test parameter values for testIsValidMoveOperation() + */ + function dataTestIsValidMoveOperation() { + return array( + array( 'Test', 'Test', 'selfmove' ), + array( 'File:Test.jpg', 'Page', 'imagenocrossnamespace' ) + ); + } + + /** + * Auth-less test of Title::userCan + * + * @param array $whitelistRegexp + * @param string $source + * @param string $action + * @param array|string|true $expected Required error + * + * @covers Title::checkReadPermission + * @dataProvider dataWgWhitelistReadRegexp + */ + function testWgWhitelistReadRegexp($whitelistRegexp, $source, $action, $expected) { + + // $wgWhitelistReadRegexp must be an array. Since the provided test cases + // usually have only one regex, it is more concise to write the lonely regex + // as a string. Thus we cast to an array() to honor $wgWhitelistReadRegexp + // type requisite. + if( is_string( $whitelistRegexp ) ) { + $whitelistRegexp = array( $whitelistRegexp ); + } + + $title = Title::newFromDBkey( $source ); + + global $wgGroupPermissions; + $oldPermissions = $wgGroupPermissions; + // Disallow all so we can ensure our regex works + $wgGroupPermissions = array(); + $wgGroupPermissions['*']['read'] = false; + + global $wgWhitelistRead; + $oldWhitelist = $wgWhitelistRead; + // Undo any LocalSettings explicite whitelists so they won't cause a + // failing test to succeed. Set it to some random non sense just + // to make sure we properly test Title::checkReadPermissions() + $wgWhitelistRead = array( 'some random non sense title' ); + + global $wgWhitelistReadRegexp; + $oldWhitelistRegexp = $wgWhitelistReadRegexp; + $wgWhitelistReadRegexp = $whitelistRegexp ; + + // Just use $wgUser which in test is a user object for '127.0.0.1' + global $wgUser; + // Invalidate user rights cache to take in account $wgGroupPermissions + // change above. + $wgUser->clearInstanceCache(); + $errors = $title->userCan( $action, $wgUser ); + + // Restore globals + $wgGroupPermissions = $oldPermissions; + $wgWhitelistRead = $oldWhitelist; + $wgWhitelistReadRegexp = $oldWhitelistRegexp; + + if( is_bool( $expected ) ) { + # Forge the assertion message depending on the assertion expectation + $allowableness = $expected + ? " should be allowed" + : " should NOT be allowed" + ; + $this->assertEquals( $expected, $errors, "User action '$action' on [[$source]] $allowableness." ); + } else { + $errors = $this->flattenErrorsArray( $errors ); + foreach ( (array)$expected as $error ) { + $this->assertContains( $error, $errors ); + } + } + } + + /** + * Provides test parameter values for testWgWhitelistReadRegexp() + */ + function dataWgWhitelistReadRegexp() { + $ALLOWED = true; + $DISALLOWED = false; + + return array( + // Everything, if this doesn't work, we're really in trouble + array( '/.*/', 'Main_Page', 'read', $ALLOWED ), + array( '/.*/', 'Main_Page', 'edit', $DISALLOWED ), + + // We validate against the title name, not the db key + array( '/^Main_Page$/', 'Main_Page', 'read', $DISALLOWED ), + // Main page + array( '/^Main/', 'Main_Page', 'read', $ALLOWED ), + array( '/^Main.*/', 'Main_Page', 'read', $ALLOWED ), + // With spaces + array( '/Mic\sCheck/', 'Mic Check', 'read', $ALLOWED ), + // Unicode multibyte + // ...without unicode modifier + array( '/Unicode Test . Yes/', 'Unicode Test Ñ Yes', 'read', $DISALLOWED ), + // ...with unicode modifier + array( '/Unicode Test . Yes/u', 'Unicode Test Ñ Yes', 'read', $ALLOWED ), + // Case insensitive + array( '/MiC ChEcK/', 'mic check', 'read', $DISALLOWED ), + array( '/MiC ChEcK/i', 'mic check', 'read', $ALLOWED ), + + // From DefaultSettings.php: + array( "@^UsEr.*@i", 'User is banned', 'read', $ALLOWED ), + array( "@^UsEr.*@i", 'User:John Doe', 'read', $ALLOWED ), + + // With namespaces: + array( '/^Special:NewPages$/', 'Special:NewPages', 'read', $ALLOWED ), + array( null, 'Special:Newpages', 'read', $DISALLOWED ), + + ); + } + function flattenErrorsArray( $errors ) { $result = array(); foreach ( $errors as $error ) { -- 2.20.1