Extension:Hierarchical Namespace Permissions/Code

From MediaWiki.org
Jump to: navigation, search
<?php
/* 
 * HierarchicalNamespacePermissions.php
 * MediaWiki extension
 * 
 * Provides an hierarchical (aka Prefix based) permission
 * sub-system for Mediawiki.
 *
 * @author: Jean-Lou Dupont
 *
 * TOP LEVEL NAMESPACES
 * ====================
 * Must be created through the standard MW way i.e.
 *
 * define('NS_ADMIN', 100);
 * $wgExtraNamespaces = array (NS_ADMIN => "Admin" );
 *
 * Hierarchical Namespace Permission atomic unit description
 * =========================================================
 *
 * [namespace:page => action]
 *
 * - Where "page" can (and obviously benefits) from having an
 * hierarchical structure e.g. top\sub1\pageXYZ
 * - Where "page" can support the "~" wildcard e.g.
 *   top\sub1\~
 * - Where "action" is any: this extension does not interpret
 *   the meaning of a particular action. This is of course left
 *   to the users of this extension.
 *   Just as example, let's consider the usual MW actions:
 *   ( 'read', 'edit', 'move', 'create' )
 *
 * - The wildcard "~" can only be used in the following patterns:
 *   a)  [ ~:~       => action ]
 *   b)  [ ns:~      => action ]
 *   c)  [ ns:page/~ => action ]
 *
 *   Where "page" can be:
 *   1) title            (just one title name)
 *   2) title/title2     (a sub-page)
 *
 * - The User Rights managed by this extension can be stored
 *   in the database OR dynamically added through "LocalSettings.php". 
 * 
 * IMPLEMENTATION NOTES:
 * =====================
 *
 * 1) The permission (aka 'right') is stored in the 'user_groups' table 
 *    using the following syntax:
 *    Each record consists of one "group" right (as standard MW)
 *    where each "group right" is formatted:
 *   
 *    "ns|NAMESPACE|PAGE|ACTION"
 *
 *    The pipe symbol | was chosen because it is considered 
 *    illegal in a MW title; this helps create a "barrier" between
 *    the normal MW title semantic and the semantic implemented
 *    in this extension.
 *
 * 2) The wildcard character used is ~ as it is illegal in a MW title
 *    and not used as a command code in PHP PCRE (which makes
 *    overall implementation much simpler)
 *
 *
 * FEATURES:
 * =========
 *  - No new database table required: all "prefixes" are stored
 *    in the existing 'user_groups' table.
 *
 *  - Adds new right level "SubmitWithoutRead" whereas a user
 *    can have the right to create/edit a page without having the right
 *    to view it. This is especially useful when processing "forms".
 *    This feature is necessary since MW (at least in v1.8.2) does not
 *    allow a form that requires creation of a new page to be posted
 *    without the user having the 'read' right. 
 *    See "wiki.php\preliminaryChecks" 
 *
 *  - Ability to explicitly give "exclude" rights.
 *    E.g.
 *      $wgGroupPermissions['sysop' ][hnpClass::buildPermissionKey("~","~","~")]    = true;
 *      $wgGroupPermissions['sysop' ][hnpClass::buildPermissionKey("~","~","!bot")] = true;
 *   Simple sysop definition where the sysop group gets everyright EXCEPT the "bot" right.
 *   This is especially useful when "editing" pages as MW will mark "rc_bot = true" in 
 *   the "recentchanges" table, thus preventing a standard view on the "Recent Changes"
 *   special page.
 
         - Ability to define a 'group hierarchy'
           e.g. sysop -> user -> *
           'sysop' rights have precedence over 'user' rights, which in turn
           have precedence over '*'
 
 *
 * MEDIAWIKI NOTES:
 * ================
 * 1) Only the forward slash is interpreted (when enabled) as an
 *    indicator of "sub-page". Thus, only the forward slash and ~ are
 *    considered here to be part of the hierarchical functionality.
 * 
 * 2) The allowed characters in a title are defined in "DefaultSettings.php".
 *    $wgLegalTitleChars = " %!\"$&'()*,\\-.\\/0-9:;=?@A-Z\\\\^_`a-z~\\x80-\\xFF+";
 *    NOTE that #{}[]| are not part of this list.
 *  
 * 3) Special command characters for preg_match do not include the following:
 *    #`'~,@
 *    Those were tested with preg_quote
 *
 * 4) Be careful not to choose namespace name identifiers too long as the 
 *    standard limit in Mediawiki is 16characters long.  This extension
 *    automatically creates the "top level namespace managers" with the 
 *    the following pattern:
 *                "Namespace Canonical Name|Mng"
 *    The total length of this identifier is not allowed to be greated than
 *    16 characters long and as such the limit for the Namespace Canonical Name
 *    is thus limited to 12characters.
 *
 * Version 1.0:
 *  - Initial availability
 * Version 1.1:
 *  - Changed name identifier for automatically created "top level manager" groups
 *    in order to respect MW's "ug_group" field in the database table "user_groups"
 *    16 characters limitation.
 * Version 1.2:
 *  - Added "exclude action" functionality through the "!" metacharacter in the 
 *    "action" field.
 *  - Corrected some corner cases
 * -------------------------------
 * Moved to BizzWiki project
 *  
 *  - added singleton functionality
 *  - added hook support for 'UserIsAllowed'
 *  - added namespace level action checking.
 *  - TODO add namespace-independant right checking.
 *  - added group hierarchy functionality.
 */
 
        // instantiate one
hnpClass::singleton();
 
class hnpClass
{
        var $lNsD; // namespace dependant rights list
        var $lNsI; // namespace independant rights list
        static $groupHierarchy; 
 
        public static function &singleton() 
        {
                static $instance;
                if ( !isset( $instance ) ) 
                        $instance = new hnpClass( );
                return $instance;
        }
 
        function hnpClass()
        {
                $this->lNsD = array();
                $this->lNsI = array();
 
                global $wgExtensionCredits;
 
                $wgExtensionCredits['other'][] = array(
                    'name'    => "HierarchicalNamespacePermissions",
                        'version' => '$Id: HierarchicalNamespacePermissions.php 152 2007-06-13 18:04:46Z jeanlou.dupont $',
                        'author'  => 'Jean-Lou Dupont' 
                );
 
 
                global $wgHooks;
                global $hnpObjDebug;
 
                if (!$hnpObjDebug)
                {
                        $wgHooks['userCan'][] = array( $this, 'userCan' );
                        $wgHooks['UserIsAllowed'][] = array( $this, 'hUserIsAllowed' );
                }
 
                $this->initGroups();
 
                // default hierarchy
                self::$groupHierarchy = array ( 'sysop', 'user', '*' );
        }
        public function addNamespaceDependantRights( $rights )   
        { 
                $this->lNsD = array_merge( $rights, $this->lNsD ); 
        }
        public function addNamespaceIndependantRights( $rights ) 
        { 
                $this->lNsI = array_merge( $rights, $this->lNsI ); 
        }
        public function setGroupHierarchy( $gh ) { self::$groupHierarchy = $gh; }
 
        function hUserIsAllowed( &$user, $ns=null, $titre=null, &$action, &$result )
        {
                $result = false; // disallow by default.
                if ($action == '') return true;
 
                // Namespace independant right ??
                if ( in_array( $action, $this->lNsI ) )
                {
                        $result = hnpClass::userCanInternal( $user, '~', '~' , $action );
                        return false;   
                }
 
                // debugging...
                if (! in_array( $action, $this->lNsD) )
                {
                        echo 'hnpClass: action <b>'.$action.'</b> not found in namespace dependant array. <br/>';
                        return false;   
                }
 
                // Namespace dependant right:
                // Two cases:
                // 1) the request comes from a stock Mediawiki method that does not know about hnpClass
                //    * request might come from a SpecialPage context.
                //
                // 2) the request comes from an hnpClass aware method somewhere.
 
                // are we asked to check for a specific action in a specific namespace??
 
                global $wgTitle;
                // Does the request come from NS_SPECIAL and namespace dependant??
                $cns = $wgTitle->getNamespace();
                $cti = $wgTitle->mDbkeyform;
 
                if ( ($cns == NS_SPECIAL) && ($ns === null) )
                {
                        echo 'hnpClass: action <b>'.$action.'</b> namespace dependant but called from NS_SPECIAL. <br/>';
                        return false;   
                }
 
                // Finally, the request comes from a valid namespace & with a valid namespace dependant action
                if ( $ns === null )    $ns = $cns;
                if ( $titre === null ) $titre = $cti;
 
                $result = hnpClass::userCanInternal( $user, $ns, $titre , $action );
 
                return false;
        }
    function userCanStub( &$t, &$u, $a, &$r )
        {
                $r = true;
                return false;
        }
 
        // t-> title, u-> user, a-> action, r-> result
        function userCan( &$t, &$u, $a, &$r )
        {
                // Check if we have a case of "page creation/edition"
                // Form posting support function.
                $submit = hnpClass::isRequestToSubmit();
 
                // Can the user perform a read operation?
                $ns = $t->getNamespace();
                $pt = $t->mDbkeyform;
 
                // Is the user allowed to post a form for
                // creation/update even without 'read' right?
                if ( $submit && ($a == 'read') )
                        $a = "SubmitWithoutRead";
 
                #echo " Namespace: $ns  Title=$pt  Action=$a \n <br/>";

                // Normal processing path.
                $r = hnpClass::userCanInternal( $u, $ns, $pt, $a );
 
                // don't let other extensions override this result.                     
                return false; 
        }
        static function userCanX( $ns, $pt, $a )
        { 
                global $wgUser;
                return hnpClass::userCanInternal($wgUser, $ns, $pt, $a); 
        }
 
        /*
         * The complex processing takes place here.
        */
        static function userCanInternal( $user, $ns, $pt, $a )
        {
                // NOTE: the term "group" is somewhat confusing.
                //       Use the following semantic to interpret:
                //       " User X is part of Group Y if X can
                //        perform Action A on the Page T of
                //        Namespace NS "
                // A User with Rights in the sub-space X\Y\* (as example)
                // is entitled *only* (assuming no other superset group is
                // defined for this User) to this sub-space i.e.
                // User can not have access to higher level pages e.g. X\*
                //
 
                foreach ( self::$groupHierarchy as $index => $group )
                {
                        // is the user part of the group?
                        if ( !self::isUserPartOfGroup( $user, $group ) ) continue;
 
                        $groupa = array( $group );
                        $grights = $user->getGroupPermissions( $groupa ); 
 
                        // FIRST GROUP OF TESTS
                        //   EXCLUDE ACTION tests
                        $rights = hnpClass::prepareRightsTable( $grights, false );
                        $eqs = hnpClass::buildPermissionKey( $ns, $pt, "!${a}" );               
                        $r = hnpClass::testRightsWildcard( $eqs, $rights );
                        if ($r) return false;           
 
                        // SECOND GROUP OF TESTS
                        // ---------------------
                        // Go through all the group membership and
                        // extract the rights looking for the ones
                        // dynamically created (e.g. by this extension i.e. createGroups)
                        // which are compatible with this extension.
                        $rights = hnpClass::prepareRightsTable( $grights );
                        $qs = hnpClass::buildPermissionKey( $ns, $pt, $a );
                        $r = hnpClass::testRightsWildcard( $qs, $rights );
                        if ($r) return true;            
                }
 
                // If all tests fail, then conclude the user does not have the require right.
                return false;
        }
        /*
         * Translate the User's Groups array format to one compatible
         * with the matching function. 
        */
        static function prepareRightsTable( $rights, $wildAction = true )
        {
                if (empty($rights))
                        return null;
 
                // Go through each group record and:
                // 0) Make sure we are dealing with a 'valid' (as per
                //    this extension semantic) group.
                //    We must also support the base groups of MW,
                //    namely '*' and 'user'.
                // 1) Translate the "~" wildcard to (.*)
                // 2) Escape all PCRE command characters
                // 3) Add the required pattern syntax characters
                //    for preg_match
                // 4) Espace the forward slash / used to make up
                //    the relative/hierarchical subspaces.
                // 5) Put the begining "ns|" has non-capturing pattern.
                // 6) Get rid of "~" action rights IF required.
 
                $index = 0 ;
                $r = array();
                foreach ($rights as $a)
                {
                        if (preg_match("/^ns/", $a)==0)           # 0
                                continue;
                        if ($wildAction == false)
                        {
                                $wa = preg_match("/~$/",$a);
                                if ($wa) continue;
                        }
 
                        $b = preg_quote( $a );                    # 2
                        $bb= preg_replace("/ns/","(?:ns)", $b);  # 5
                        $c = preg_replace("/\//", "\/" , $bb);      # 4
                        $e = preg_replace( "/~/", "(.*)", $c );    # 1
                        $r[$index] = "/^".$e."$/";                 # 3
                        $index++;
                }
                return $r;
        }
 
        static function testRightsWildcard( $q, $rights )
        {       
                if (empty($rights))
                        return false;
 
                // Go through each right
                // and look if the query matches with it
                // In reality, the array $rights is (should be!) already
                // formatted for use with the matching function, acting as
                // the pattern in question.
                foreach ($rights as $pattern)
                        if ( preg_match( $pattern, $q ) > 0 )
                                return true;    
 
                return false;           
        }
 
        /*
         * Is the user posted a form that requires
         * creation/updating a wiki page?
        */
        static function isRequestToSubmit()
        {
                global    $wgRequest;
                $action = $wgRequest->getVal( 'wpSave', 'view' );
                return    ($action == "submit" ? true:false);
        }
 
        static function buildPermissionKey( $ns, $titre, $action )
        {       return "ns|{$ns}|{$titre}|{$action}";   }
 
        /*
        *  Create default groups
        */
        function initGroups()
        {
                global $wgGroupPermissions, $wgCanonicalNamespaceNames;
 
                /* For each Ns, create a "manager" group having
                 * every right in the namespace.
                */
                foreach( $wgCanonicalNamespaceNames as $num => $titre )
                {
                $wgGroupPermissions[ "Gr|{$num}|NsMng" ][ "ns|{$titre}|~|~"   ] = true;
                $wgGroupPermissions[ "Gr|{$num}|NsMng" ][ "ns|{$num}|~|~"     ] = true;
                }
 
        }
        public static function isUserPartOfGroup( &$user, $group )
        {
                if (empty( $group )) return false;
 
                return in_array( $group, $user->getEffectiveGroups() );
        }
 
} # end class definition

?>
Personal tools
Namespaces

Variants
Actions
Navigation
Support
Download
Development
Communication
Print/export
Toolbox