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

?>