Difference between revisions of "Extension:SimpleSecurity"

From Organic Design wiki
(Move directive processing into validateTitle() and call from onGetUserRights())
(Slim down to new road map (may need some code from history again later tho))
Line 1: Line 1:
 
<?php
 
<?php
 +
/**
 +
* Simple Security extension
 +
* - Extends the MediaWiki article protection to allow restricting viewing of article content
 +
* - Also adds #ifusercan and #ifgroup parser functions for rendering restriction-based content
 +
*
 +
* See http://www.mediawiki.org/Extension:Simple_Security for installation and usage details
 +
* See http://www.organicdesign.co.nz/Extension_talk:SimpleSecurity4.php for development notes and disucssion
 +
* Version 4.0.0 started 2007-10-11
 +
* Version 4.1.0 started 2008-06-12 (development funded for a slimmed down functional version)
 +
*
 +
* @package MediaWiki
 +
* @subpackage Extensions
 +
* @author Aran Dunkley [http://www.organicdesign.co.nz/nad User:Nad]
 +
* @copyright © 2007 Aran Dunkley
 +
* @licence GNU General Public Licence 2.0 or later
 +
*/
 
# Simple security extension{{php}}{{Category:Extensions|SimpleSecurity4}}{{Category:Extensions in progress|SimpleSecurity4}}
 
# Simple security extension{{php}}{{Category:Extensions|SimpleSecurity4}}{{Category:Extensions in progress|SimpleSecurity4}}
 
# - See http://www.mediawiki.org/Extension:Simple_Security for installation and usage details
 
# - See http://www.mediawiki.org/Extension:Simple_Security for installation and usage details
Line 5: Line 21:
 
# - Needs apache's mod-rewrite for security on images, see code comments below
 
# - Needs apache's mod-rewrite for security on images, see code comments below
 
# - Version 4.0.0 started 2007-10-11
 
# - Version 4.0.0 started 2007-10-11
 +
#
  
 
if (!defined('MEDIAWIKI')) die('Not an entry point.');
 
if (!defined('MEDIAWIKI')) die('Not an entry point.');
  
define('SIMPLESECURITY_VERSION','4.0.9, 2008-01-04');
+
define('SIMPLESECURITY_VERSION','4.1.0, 2008-06-12');
  
 
# Global security settings
 
# Global security settings
$wgSecurityPermsMagic    = "permissions";                      # the parser-function name for setting group permissions in articles
 
$wgSecuritySecurityMagic = "security";                        # the parser-function name for old-style (<4.x) security directives
 
 
$wgSecurityMagicIf      = "ifusercan";                        # the name for doing a permission-based conditional
 
$wgSecurityMagicIf      = "ifusercan";                        # the name for doing a permission-based conditional
 
$wgSecurityMagicGroup    = "ifgroup";                          # the name for doing a group-based conditional
 
$wgSecurityMagicGroup    = "ifgroup";                          # the name for doing a group-based conditional
$wgSecurityDenyTemplate  = 'Template:Action not permitted';    # Template used for displaying error when user violates permissions
+
$wgSecurityLogActions    = array('edit', 'download');         # Actions that should be logged
$wgSecurityInfoTemplate  = 'Template:Security info';          # Template used to layout the security information
+
$wgSecurityUseDBHook     = false;                             # Add the DatabaseFetchHook to validate database access
$wgSecurityLogActions    = array('download');                 # Actions that should be logged
+
$wgSecurityAllowUser     = false;                             # Allow restrictions based on user not just group
$wgSecurityDenyImage     = "/skins/monobook/lock_icon.gif";   # the image returned in place of requested image if read access denied
+
 
$wgSecurityUseDBHook     = true;                               # Add the DatabaseFetchHook to validate database access (experimental!)
+
 
 +
array_unshift($wgExtensionFunctions, 'wfSetupSimpleSecurity'); # Put SimpleSecurity's setup function before all others
  
array_unshift($wgExtensionFunctions,'wfSetupSimpleSecurity');
 
 
$wgHooks['LanguageGetMagic'][] = 'wfSimpleSecurityLanguageGetMagic';
 
$wgHooks['LanguageGetMagic'][] = 'wfSimpleSecurityLanguageGetMagic';
 
$wgExtensionCredits['parserhook'][] = array(
 
$wgExtensionCredits['parserhook'][] = array(
 
'name'        => "Simple Security",
 
'name'        => "Simple Security",
 
'author'      => '[http://www.organicdesign.co.nz/User:Nad User:Nad]',
 
'author'      => '[http://www.organicdesign.co.nz/User:Nad User:Nad]',
'description' => 'A simple to implement security extension',
+
'description' => 'Extends the MediaWiki article protection to allow restricting viewing of article content',
 
'url'        => 'http://www.mediawiki.org/wiki/Extension:Simple_Security',
 
'url'        => 'http://www.mediawiki.org/wiki/Extension:Simple_Security',
 
'version'    => SIMPLESECURITY_VERSION
 
'version'    => SIMPLESECURITY_VERSION
Line 33: Line 48:
 
class SimpleSecurity {
 
class SimpleSecurity {
  
var $processDirectives = false; # whether or not to process security directives when parsing wikitext
+
/**
var $permissions = array();    # permissions obtained from security directives (same format as $wgGroupPermissions)
+
* Constructor
var $rights = array();          # the current users rights after processing $wgGroupPermissions
+
*/
var $info = array();            # accumulated security information accumulated from both directives and $wgGroupPermissions (array of $group,$action,$allow,$comment)
 
 
 
# Constructor
 
 
function __construct() {
 
function __construct() {
global $wgParser,$wgHooks,$wgLogTypes,$wgLogNames,$wgLogHeaders,$wgLogActions,
+
global $wgParser, $wgHooks, $wgLogTypes, $wgLogNames, $wgLogHeaders, $wgLogActions,
$wgSecuritySecurityMagic,$wgSecurityPermsMagic,$wgSecurityMagicIf,$wgSecurityMagicGroup;
+
$wgSecurityMagicIf, $wgSecurityMagicGroup;
  
 
# Add our parser-hooks
 
# Add our parser-hooks
$wgParser->setFunctionHook($wgSecuritySecurityMagic,array($this,'security'));
+
$wgParser->setFunctionHook($wgSecurityMagicIf, array($this,'ifUserCan'));
$wgParser->setFunctionHook($wgSecurityPermsMagic,array($this,'permissions'));
+
$wgParser->setFunctionHook($wgSecurityMagicGroup, array($this,'ifGroup'));
$wgParser->setFunctionHook($wgSecurityMagicIf,array($this,'ifUserCan'));
 
$wgParser->setFunctionHook($wgSecurityMagicGroup,array($this,'ifGroup'));
 
 
 
# Add all our required event hooks
 
$wgHooks['UserGetRights'][]        = $this;
 
$wgHooks['UserEffectiveGroups'][]  = $this;
 
$wgHooks['OutputPageBeforeHTML'][] = $this;
 
  
 
# Add a new log type
 
# Add a new log type
Line 59: Line 64:
 
$wgLogHeaders['security']      = 'securitylogpagetext';
 
$wgLogHeaders['security']      = 'securitylogpagetext';
 
$wgLogActions['security/deny'] = 'securitylogentry';
 
$wgLogActions['security/deny'] = 'securitylogentry';
}
+
}
  
# Enable/disable directive processing
+
/**
# - $this->permissions is cleared when enabled
+
* Process the ifUserCan conditional security directive
private function setDirectiveProcessing($enabled) {
+
*/
$this->processDirectives = $enabled;
+
public function ifUserCan(&$parser, $action, $title, $then, $else = '') {
if ($enabled) $this->permissions = array();
+
return $this->validateTitle($action, $title) ? $then : $else;
}
+
}
 
 
# Process a permissions directive and save in $this->permissions (same format as $wgGroupPermissions)
 
private function permissions(&$parser,$actions = '',$groups = '',$allow = '') {
 
$parser->disableCache(); # Dont cache anything containing permissions directives
 
if ($actions == '' || $groups == '') return wfMsg('securitysyntaxerror');
 
if ($this->processDirectives) {
 
# todo
 
}
 
return '';
 
}
 
 
 
# Process an old-style (<4.x) security directive and save in $this->permissions (same format as $wgGroupPermissions)
 
private function security(&$parser,$actions = '',$groups = '',$allow = '') {
 
$parser->disableCache(); # Dont cache anything containing security directives
 
if ($actions == '' || $groups == '') return wfMsg('securitysyntaxerror');
 
if ($this->processDirectives) {
 
$allow  = eregi('^(0|false)$',$allow) ? false : true;
 
$actions = preg_split('/\\s*,\\s*/',$actions,-1,PREG_SPLIT_NO_EMPTY);
 
$groups  = preg_split('/\\s*,\\s*/',$groups,-1,PREG_SPLIT_NO_EMPTY);
 
foreach ($groups as $g) foreach ($actions as $a) $this->permissions[$g][$a] = $allow;
 
}
 
return '';
 
}
 
 
 
# Process the ifUserCan conditional security directive
 
public function ifUserCan(&$parser,$action,$title,$then,$else = '') {
 
return $this->validateTitle($action,$title) ? $then : $else;
 
}
 
  
# Process the ifGroup conditional security directive
+
/**
# - evaluates to true if current uset belongs to any of the comma-separated users and/or groups in the first parameter
+
* Process the ifGroup conditional security directive
public function ifGroup(&$parser,$groups,$then,$else = '') {
+
* - evaluates to true if current uset belongs to any of the comma-separated users and/or groups in the first parameter
 +
*/
 +
public function ifGroup(&$parser, $groups, $then, $else = '') {
 
global $wgUser;
 
global $wgUser;
$intersection = array_intersect(array_map('strtolower',split(',',$groups)),$wgUser->getEffectiveGroups());
+
$intersection = array_intersect(array_map('strtolower', split(',', $groups)), $wgUser->getEffectiveGroups());
 
return count($intersection) > 0 ? $then : $else;
 
return count($intersection) > 0 ? $then : $else;
}
+
}
 
 
# Ensure all groups are lowercase and add the username with first letter capitalised
 
public function onUserEffectiveGroups(&$user,&$groups) {
 
$groups = array_map('strtolower',$groups);
 
$groups[] = ucfirst($user->getName());
 
return true;
 
}
 
 
 
# Intercept the user rights and filter based on $wgGroupPermissions and $wgTitle
 
# - this hook is called once the first time User::getRights is called
 
# - the passed $rights array is a merge of $wgGroupPermissions and User::getEffectiveGroups()
 
# - $this->info is populated in this method
 
public function onUserGetRights(&$user,&$rights) {
 
global $wgGroupPermissions,$wgTitle;
 
if (!is_object($wgTitle)) return true;
 
 
 
# Add x:x style permissions (Category, Namespace, Title, Match) to list of perms to process
 
$cats = array();
 
foreach ($wgGroupPermissions as $k => $perms) if (preg_match('/^(.+?):(.*)$/',$k,$m)) {
 
$type  = strtolower($m[1]);
 
$data  = $m[2];
 
$merge = false;
 
 
 
# Check each kind of x:x rule against the current key
 
switch ($type) {
 
case "category":
 
if (count($cats) == 0) {
 
# If processing first category rule, build a list of cats this article belongs to
 
$dbr = &wfGetDB(DB_SLAVE);
 
$cl  = $dbr->tableName('categorylinks');
 
$id  = $wgTitle->getArticleID();
 
$res = $dbr->select($cl,'cl_to',"cl_from = '$id'",__METHOD__,array('ORDER BY' => 'cl_sortkey'));
 
while ($row = $dbr->fetchObject($res)) $cats[] == $row[0];
 
$dbr->freeResult($res);
 
}
 
$merge = in_array($cats,$data);
 
break;
 
case "namespace":
 
$merge = $data == $wgTitle->getNamespace();
 
break;
 
case "title":
 
$merge = $data == $wgTitle->getText();
 
break;
 
case "regex":
 
$merge = preg_match($data,$wgTitle->getText());
 
break;
 
 
 
# If x:x type unknown, call SimpleSecurityPermissionFilter hook to allow other kinds
 
# - the hook should set $merge to true based on whether $type:$data applies to $user
 
default: wfRunHooks('SimpleSecurityPermissionFilter',array($type,$data,&$merge,&$user,&$wgTitle));
 
}
 
 
 
# If current $wgGroupPermissions rule matches, merge it into the rights array
 
if ($merge) self::mergeRights($user,$rights,$perms,"$type:$data");
 
}
 
 
# Remember rights list before applying title-based validation
 
$this->rights = $rights;
 
 
 
# Parse the article with directive-processing enabled and update the rights
 
$rights = $this->validateTitle($action,$wgTitle,'Parser Function');
 
 
 
return true;
 
}
 
 
 
# Update a rights-list based on passed user and permissions array
 
# - pre-existing rights in the list can be removed since each permission has group, action and allow
 
static function mergeRights(&$user,&$rights,&$permissions,$comment = false) {
 
$groups = $this->getEffectiveGroups();
 
foreach ($groups as $group)
 
if (isset($permissions[$group]))
 
foreach ($permissions[$group] as $action => $allow) {
 
# Add or remove a right from the list
 
if ($allow && !in_array($action,$rights)) $rights[] = $action;
 
elseif (!$allow && in_array($action,$rights)) array_splice($rights,array_search($action,$rights),1);
 
$this->info[] = array($group,$action,$allow,$comment);
 
}
 
}
 
  
# Render security info
+
/**
public function onOutputPageBeforeHTML(&$out,&$text) {
+
* Validate the passed database row and replace any invalid content
global $wgUser,$wgTitle,$wgVersion,$wgSiteNotice,$wgScriptPath,$wgSecurityDenyImage;
+
* - this does not get called if already processing directives
 
+
*/
# Add rules information for this article if any
 
if ($this->info) {
 
 
 
# Parser the security info
 
$info = $this->info;
 
$this->info = false;
 
$parser = new Parser;
 
$info = $parser->parse($info,$wgTitle,ParserOptions::newFromUser($wgUser),true,true)->getText();
 
 
 
# Add some javascript to allow toggling the security-info
 
$out->addScript("<script type='text/javascript'>
 
function toggleSecurityInfo() {
 
var info = document.getElementById('security-info');
 
info.style.display = info.style.display ? '' : 'none';
 
}
 
</script>");
 
 
 
# Add info-toggle before title and hidden info after title
 
$pagetitle = $wgTitle->getText();
 
$alt = wfMsg('securityinfotoggle');
 
$toggle = "<span id='security-info-toggle' onClick='toggleSecurityInfo()' title='$alt'></span>";
 
$info = "<div id='security-info' style='display:none'>Security info:<br>$info</div>";
 
$out->setPageTitle($toggle.$pagetitle.$info);
 
$out->setHTMLTitle($pagetitle);
 
#$out->mPageLinkTitle = $pagetitle;
 
}
 
 
 
return true;
 
}
 
 
 
# Validate an action against a title within the context of the current user
 
# - refines $this->rights which is the current users rights after processing $wgGroupPermissions
 
private function validateTitle($action,&$title,$comment = '') {
 
$rights = $this->rights;
 
if (!$title->exists()) return $rights;
 
global $wgParser,$wgUser;
 
 
 
# Parse the article with directive-processing enabled and update the rights
 
if (is_object($wgParser)) { $psr = $wgParser; $opt = $wgParser->mOptions; }
 
else { $psr = new Parser; $opt = NULL; }
 
if (!is_object($opt)) $opt = ParserOptions::newFromUser($wgUser);
 
$this->setDirectiveProcessing(true);
 
$article = new Article($title);
 
$psr->preprocess($article->getContent(),$title,$opt);
 
$this->setDirectiveProcessing(false);
 
self::mergeRights($wgUser,$rights,$this->permissions,$comment);
 
 
 
# For backward compatibility with old SimpleSecurity directives
 
if (in_array('view',$rights) && !in_array('read',$rights)) $rights[] = 'read';
 
 
 
return $rights;
 
}
 
 
 
# Validate the passed database row and replace any invalid content
 
# - this does not get called if already processing directives
 
 
private function validateDatabaseRow(&$row) {
 
private function validateDatabaseRow(&$row) {
global $wgUser,$wgSecuritySysops;
+
# TODO
 
 
# todo: find the current revision associated with the old_id
 
 
 
$this->setDirectiveProcessing(true);
 
# todo: read and parse the revision text
 
$this->setDirectiveProcessing(false);
 
 
 
# todo: validate the row and replace with wfMsg('securityreaddeny') if invalid
 
 
 
}
 
 
 
# Needed in some versions to prevent Special:Version from breaking
 
public function __toString() { return __CLASS__; }
 
 
}
 
}
  
# Adds UserGetRights and UserEffectiveGroups for MediaWiki versions prior to 1.11 which don't have them
+
/**
function wfSimpleSecurityAddUserHooks() {
+
* Needed in some versions to prevent Special:Version from breaking
global $wgVersion,$wgUser;
+
*/
if (version_compare($wgVersion,"1.11.0") <= 0) return;
+
public function __toString() {
 
+
return __CLASS__;
# Create a new User class (User2) by extending the existing one with overridden methods
 
class User2 extends User {
 
function getRights() {
 
parent::getRights();
 
wfRunHooks("UserGetRights",array($this,&$this->mRights));
 
}
 
function getEffectiveGroups($recache = false) {
 
$groups = parent::getEffectiveGroups($recache);
 
wfRunHooks('UserEffectiveGroups',array(&$this,&$groups));
 
return $groups;
 
}
 
}
 
 
 
# If $wgUser is not of class User, it must be a StubUser which means we need to override _newObject to return a User2 instance
 
$userclass = get_class($wgUser);
 
if ($userclass != 'User') {
 
class StubUser2 extends StubUser {
 
function _newObject() {
 
return wfCreateObject('User2',$this->mParams);
 
}
 
}
 
}
 
 
 
# Replace $wgUser with a sub-classed replica
 
$oldUser  = $wgUser;
 
$newclass = $userclass.'2';
 
$wgUser  = new $newclass();
 
foreach (array_keys(get_class_vars($userclass)) as $k) $wgUser->$k = $oldUser->$k;
 
 
}
 
}
 +
}
  
# Hooks into Database::query and Database::fetchObject via the LoadBalancer class
+
/**
 +
* Hooks into Database::query and Database::fetchObject via the LoadBalancer class
 +
*/
 
function wfSimpleSecurityAddDatabaseHooks() {
 
function wfSimpleSecurityAddDatabaseHooks() {
 
global $wgLoadBalancer,$wgDBtype;
 
global $wgLoadBalancer,$wgDBtype;
Line 308: Line 116:
 
eval("class Database{$type}2 extends Database{$type}".' {
 
eval("class Database{$type}2 extends Database{$type}".' {
  
public function query($sql,$fname = "",$tempIgnore = false) {
+
public function query($sql, $fname = "", $tempIgnore = false) {
 
global $wgSimpleSecurity;
 
global $wgSimpleSecurity;
 
$count = false;
 
$count = false;
 
if (!$wgSimpleSecurity->processDirectives)
 
if (!$wgSimpleSecurity->processDirectives)
$patched = preg_replace_callback("/(?<=SELECT ).+?(?= FROM)/","wfSimpleSecurityPatchSQL",$sql,1,$count);
+
$patched = preg_replace_callback("/(?<=SELECT ).+?(?= FROM)/", "wfSimpleSecurityPatchSQL", $sql, 1, $count);
 
return parent::query($count ? $patched : $sql,$fname,$tempIgnore);
 
return parent::query($count ? $patched : $sql,$fname,$tempIgnore);
 
}
 
}
Line 331: Line 139:
 
$db =& parent::reallyOpenConnection($server);
 
$db =& parent::reallyOpenConnection($server);
 
return $db;
 
return $db;
}
 
 
}
 
}
 +
}
  
 
# Replace the $wgLoadBalancer object with an identical instance of the new LoadBalancer2 class
 
# Replace the $wgLoadBalancer object with an identical instance of the new LoadBalancer2 class
Line 340: Line 148:
 
foreach (array_keys(get_class_vars('LoadBalancer')) as $k) $wgLoadBalancer->$k = $oldLoadBalancer->$k;
 
foreach (array_keys(get_class_vars('LoadBalancer')) as $k) $wgLoadBalancer->$k = $oldLoadBalancer->$k;
  
}
+
}
  
# Patches SQL queries to ensure that the old_id field is present in all requests for the old_text field so permissions can be validated
+
/**
 +
* Patches SQL queries to ensure that the old_id field is present in all requests for the old_text field so permissions can be validated
 +
*/
 
function wfSimpleSecurityPatchSQL($match) {
 
function wfSimpleSecurityPatchSQL($match) {
if (!preg_match("/old_text/",$match[0])) return $match[0];
+
if (!preg_match("/old_text/", $match[0])) return $match[0];
$fields = str_replace(" ","",$match[0]);
+
$fields = str_replace(" ", "", $match[0]);
return ($fields == "*" || preg_match("/old_id/",$fields)) ? $fields : "$fields,old_id";
+
return ($fields == "*" || preg_match("/old_id/", $fields)) ? $fields : "$fields,old_id";
 
}
 
}
  
# Called from $wgExtensionFunctions array when initialising extensions
+
/**
 +
* Called from $wgExtensionFunctions array when initialising extensions
 +
*/
 
function wfSetupSimpleSecurity() {
 
function wfSetupSimpleSecurity() {
global $wgSimpleSecurity,$wgLanguageCode,$wgMessageCache,$wgSecurityMagic,$wgSecurityUseDBHook;
+
global $wgSimpleSecurity, $wgLanguageCode, $wgMessageCache, $wgSecurityMagic, $wgSecurityUseDBHook;
  
 
# Hooks into Database::query and Database::fetchObject via the LoadBalancer class
 
# Hooks into Database::query and Database::fetchObject via the LoadBalancer class
 
if ($wgSecurityUseDBHook) wfSimpleSecurityAddDatabaseHooks();
 
if ($wgSecurityUseDBHook) wfSimpleSecurityAddDatabaseHooks();
 
# Adds UserGetRights and UserEffectiveGroups for MediaWiki versions prior to 1.11 which don't have them
 
wfSimpleSecurityAddUserHooks();
 
  
 
# Instantiate the SimpleSecurity singleton now that the environment is prepared
 
# Instantiate the SimpleSecurity singleton now that the environment is prepared
Line 377: Line 186:
 
}
 
}
  
# Register magic words
+
/**
function wfSimpleSecurityLanguageGetMagic(&$magicWords,$langCode = 0) {
+
* Register magic words
global $wgSecuritySecurityMagic,$wgSecurityPermsMagic,$wgSecurityMagicIf,$wgSecurityMagicGroup;
+
*/
$magicWords[$wgSecurityPermsMagic]    = array(0,$wgSecurityPermsMagic);
+
function wfSimpleSecurityLanguageGetMagic(&$magicWords, $langCode = 0) {
$magicWords[$wgSecuritySecurityMagic] = array(0,$wgSecuritySecurityMagic);
+
global $wgSecurityMagicIf,$wgSecurityMagicGroup;
$magicWords[$wgSecurityMagicIf]      = array(0,$wgSecurityMagicIf);
+
$magicWords[$wgSecurityMagicIf]    = array($langCode, $wgSecurityMagicIf);
$magicWords[$wgSecurityMagicGroup]   = array(0,$wgSecurityMagicGroup);
+
$magicWords[$wgSecurityMagicGroup] = array($langCode, $wgSecurityMagicGroup);
 
return true;
 
return true;
}
+
}

Revision as of 07:04, 12 June 2008

<?php /**

* Simple Security extension
* - Extends the MediaWiki article protection to allow restricting viewing of article content
* - Also adds #ifusercan and #ifgroup parser functions for rendering restriction-based content
*
* See http://www.mediawiki.org/Extension:Simple_Security for installation and usage details
* See http://www.organicdesign.co.nz/Extension_talk:SimpleSecurity4.php for development notes and disucssion
* Version 4.0.0 started 2007-10-11
* Version 4.1.0 started 2008-06-12 (development funded for a slimmed down functional version)
* 
* @package MediaWiki
* @subpackage Extensions
* @author Aran Dunkley User:Nad
* @copyright © 2007 Aran Dunkley
* @licence GNU General Public Licence 2.0 or later
*/
  1. Simple security extensionTemplate:Php
Info.svg These are the MediaWiki extensions we're using and/or developing. Please refer to the information on the mediawiki.org wiki for installation and usage details. Extensions here which have no corresponding mediawiki article are either not ready for use or have been superseded. You can also browse our extension code in our local Subversion repository or our GitHub mirror.

Category:Extensions in progress

  1. - See http://www.mediawiki.org/Extension:Simple_Security for installation and usage details
  2. - Licenced under LGPL (http://www.gnu.org/copyleft/lesser.html)
  3. - Needs apache's mod-rewrite for security on images, see code comments below
  4. - Version 4.0.0 started 2007-10-11

if (!defined('MEDIAWIKI')) die('Not an entry point.');

define('SIMPLESECURITY_VERSION','4.1.0, 2008-06-12');

  1. Global security settings

$wgSecurityMagicIf = "ifusercan"; # the name for doing a permission-based conditional $wgSecurityMagicGroup = "ifgroup"; # the name for doing a group-based conditional $wgSecurityLogActions = array('edit', 'download'); # Actions that should be logged $wgSecurityUseDBHook = false; # Add the DatabaseFetchHook to validate database access $wgSecurityAllowUser = false; # Allow restrictions based on user not just group


array_unshift($wgExtensionFunctions, 'wfSetupSimpleSecurity'); # Put SimpleSecurity's setup function before all others

$wgHooks['LanguageGetMagic'][] = 'wfSimpleSecurityLanguageGetMagic'; $wgExtensionCredits['parserhook'][] = array( 'name' => "Simple Security", 'author' => 'User:Nad', 'description' => 'Extends the MediaWiki article protection to allow restricting viewing of article content', 'url' => 'http://www.mediawiki.org/wiki/Extension:Simple_Security', 'version' => SIMPLESECURITY_VERSION );

class SimpleSecurity {

/** * Constructor */ function __construct() { global $wgParser, $wgHooks, $wgLogTypes, $wgLogNames, $wgLogHeaders, $wgLogActions, $wgSecurityMagicIf, $wgSecurityMagicGroup;

# Add our parser-hooks $wgParser->setFunctionHook($wgSecurityMagicIf, array($this,'ifUserCan')); $wgParser->setFunctionHook($wgSecurityMagicGroup, array($this,'ifGroup'));

# Add a new log type $wgLogTypes[] = 'security'; $wgLogNames ['security'] = 'securitylogpage'; $wgLogHeaders['security'] = 'securitylogpagetext'; $wgLogActions['security/deny'] = 'securitylogentry'; }

/** * Process the ifUserCan conditional security directive */ public function ifUserCan(&$parser, $action, $title, $then, $else = ) { return $this->validateTitle($action, $title) ? $then : $else; }

/** * Process the ifGroup conditional security directive * - evaluates to true if current uset belongs to any of the comma-separated users and/or groups in the first parameter */ public function ifGroup(&$parser, $groups, $then, $else = ) { global $wgUser; $intersection = array_intersect(array_map('strtolower', split(',', $groups)), $wgUser->getEffectiveGroups()); return count($intersection) > 0 ? $then : $else; }

/** * Validate the passed database row and replace any invalid content * - this does not get called if already processing directives */ private function validateDatabaseRow(&$row) { # TODO }

/** * Needed in some versions to prevent Special:Version from breaking */ public function __toString() { return __CLASS__; } }

/**

* Hooks into Database::query and Database::fetchObject via the LoadBalancer class
*/

function wfSimpleSecurityAddDatabaseHooks() { global $wgLoadBalancer,$wgDBtype;

# This ensures that $wgLoadBalancer is not a stub object when we subclass it # todo: this should be able to work in the case of it being a stub object wfGetDB();

# Create a replica of the Database class # - query method is overriden to ensure that old_id field is returned for all queries which read old_text field # - fetchObject method is overridden to validate row content based on old_id # - the changes to this class are only active for SELECT statements and while not processing security directives $type = ucfirst($wgDBtype); eval("class Database{$type}2 extends Database{$type}".' {

public function query($sql, $fname = "", $tempIgnore = false) { global $wgSimpleSecurity; $count = false; if (!$wgSimpleSecurity->processDirectives) $patched = preg_replace_callback("/(?<=SELECT ).+?(?= FROM)/", "wfSimpleSecurityPatchSQL", $sql, 1, $count); return parent::query($count ? $patched : $sql,$fname,$tempIgnore); }

function fetchObject(&$res) { global $wgSimpleSecurity; $row = parent::fetchObject($res); if (!$wgSimpleSecurity->processDirectives) $wgSimpleSecurity->validateDatabaseRow($row); return $row; }

}');

# Create a replica of the LoadBalancer class which uses the new Database subclass for its connection objects class LoadBalancer2 extends LoadBalancer { function reallyOpenConnection(&$server) { $server['type'] .= '2'; $db =& parent::reallyOpenConnection($server); return $db; } }

# Replace the $wgLoadBalancer object with an identical instance of the new LoadBalancer2 class $wgLoadBalancer->closeAll(); # Close any open connections as they will be of the original Database class $oldLoadBalancer = $wgLoadBalancer; $wgLoadBalancer = new LoadBalancer2($oldLoadBalancer->mServers); foreach (array_keys(get_class_vars('LoadBalancer')) as $k) $wgLoadBalancer->$k = $oldLoadBalancer->$k;

}

/**

* Patches SQL queries to ensure that the old_id field is present in all requests for the old_text field so permissions can be validated
*/

function wfSimpleSecurityPatchSQL($match) { if (!preg_match("/old_text/", $match[0])) return $match[0]; $fields = str_replace(" ", "", $match[0]); return ($fields == "*" || preg_match("/old_id/", $fields)) ? $fields : "$fields,old_id"; }

/**

* Called from $wgExtensionFunctions array when initialising extensions
*/

function wfSetupSimpleSecurity() { global $wgSimpleSecurity, $wgLanguageCode, $wgMessageCache, $wgSecurityMagic, $wgSecurityUseDBHook;

# Hooks into Database::query and Database::fetchObject via the LoadBalancer class if ($wgSecurityUseDBHook) wfSimpleSecurityAddDatabaseHooks();

# Instantiate the SimpleSecurity singleton now that the environment is prepared $wgSimpleSecurity = new SimpleSecurity();

# Add the messages used by the specialpage if ($wgLanguageCode == 'en') { $wgMessageCache->addMessages(array( 'security' => "Security log", 'securitylogpage' => "Security log", 'securitylogpagetext' => "This is a log of actions blocked by the SimpleSecurity extension.", 'securitylogentry' => "", 'securityinfotoggle' => "This article exhibits security restrictions. Click this icon for more detail.", 'securityreaddeny' => "!!Permission denied!!", 'securitysyntaxerror' => "{{#$wgSecurityMagic:Error: Both action and group must be specified!}}", 'securitylogdeny' => "Attempt to $1 $2 was denied." )); } }

/**

* Register magic words
*/

function wfSimpleSecurityLanguageGetMagic(&$magicWords, $langCode = 0) { global $wgSecurityMagicIf,$wgSecurityMagicGroup; $magicWords[$wgSecurityMagicIf] = array($langCode, $wgSecurityMagicIf); $magicWords[$wgSecurityMagicGroup] = array($langCode, $wgSecurityMagicGroup); return true; }