Whitelisting publicly readable title with regex
authornullspoon <nullspoon@iohq.net>
Tue, 7 Aug 2012 22:06:12 +0000 (16:06 -0600)
committerAntoine Musso <hashar@free.fr>
Mon, 28 Jan 2013 13:38:37 +0000 (14:38 +0100)
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 <hashar@free.fr>
Change-Id: I83f6a614874c3d289ff4bd8d015f1d9c92e500b6

CREDITS
RELEASE-NOTES-1.21
includes/DefaultSettings.php
includes/Title.php
tests/phpunit/includes/TitleTest.php

diff --git a/CREDITS b/CREDITS
index a03ad5e..7da6cb6 100644 (file)
--- 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
index 1efd80d..b9fdecf 100644 (file)
@@ -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.
index d68d991..6a4f6a6 100644 (file)
@@ -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?
index 9de94d8..a3f17a7 100644 (file)
@@ -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 ) );
index 8647954..6399d48 100644 (file)
@@ -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 ) {