<?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
?>