Extension:SimpleSecurity
<?php
- Simple security extensionTemplate:Php
- - See http://www.mediawiki.org/Extension:Simple_Security for installation and usage details
- - Licenced under LGPL (http://www.gnu.org/copyleft/lesser.html)
- - Needs apache's mod-rewrite for security on images, see code comments below
- - Version 4.0.0 started 2007-10-11
if (!defined('MEDIAWIKI')) die('Not an entry point.');
define('SIMPLESECURITY_VERSION','4.0.2, 2007-10-13');
- Global security settings
$wgSecurityMagic = "security"; # the parser-function name for security directives $wgSecurityMagicInherit = "!security"; # the name for security directives which apply to member articles $wgSecurityMagicIf = "ifallowed"; # the name for doing a permission-based conditional $wgSecurityMagicGroup = "ifgroup"; # the name for doing a group-based conditional $wgSecuritySysops = array('sysop'); # the list of groups whose members bypass all security (groups a all lowercase, user are ucfirst) $wgSecurityDenyTemplate = 'Template:Action not permitted'; $wgSecurityDenyReadTemplate = 'Template:Not readable'; $wgSecurityInfoTemplate = 'Template:Security info'; $wgSecurityRuleTemplate = ; # set to a template for the $wgSecurityDenyImage = dirname(__FILE__).'/deny.png'; # the image returned in place of requested image if read access denied $wgNamespacePermissions = array(); $wgSecurityLogActions = array('download'); # Actions that should be logged
array_unshift($wgExtensionFunctions,'wfSetupSimpleSecurity'); $wgHooks['LanguageGetMagic'][] = 'wfSimpleSecurityLanguageGetMagic'; $wgExtensionCredits['parserhook'][] = array( 'name' => "Simple Security", 'author' => 'User:Nad', 'description' => 'A simple to implement security extension', 'url' => 'http://www.mediawiki.org/wiki/Extension:Simple_Security', 'version' => SIMPLESECURITY_VERSION );
class SimpleSecurity {
# Needed in some versions to prevent Special:Version from breaking function __toString() { return 'SimpleSecurity'; }
# Constructor function SimpleSecurity() { global $wgParser,$wgHooks,$wgLogTypes,$wgLogNames,$wgLogHeaders,$wgLogActions, $wgSecurityMagic,$wgSecurityMagicInherit,$wgSecurityMagicIf,$wgSecurityMagicGroup;
# Add all our required event hooks $wgHooks['userCan'][] = $this; $wgHooks['ArticleAfterFetchContent'][] = $this; $wgHooks['OutputPageBeforeHTML'][] = $this;
$wgParser->setFunctionHook($wgSecurityMagic,array($this,'processDirective')); $wgParser->setFunctionHook($wgSecurityMagicInherit,array($this,'processInheritDirective',SFH_NO_HASH)); $wgParser->setFunctionHook($wgSecurityMagicIf,array($this,'ifAllowed')); $wgParser->setFunctionHook($wgSecurityMagicGroup,array($this,'ifGroup'));
# Add a new log type $wgLogTypes[] = 'security'; $wgLogNames ['security'] = 'securitylogpage'; $wgLogHeaders['security'] = 'securitylogpagetext'; $wgLogActions['security/deny'] = 'securitylogentry'; }
# Process a normal security directive function processDirective(&$parser,$actions = ,$groups = ) { $parser->mOutput->mCacheTime = -1; if ($actions && $groups) $this->directives[] = array($actions,$groups); return ; }
# Process a non-inheriting security directive function processInheritDirective(&$parser,$actions = ,$groups = ) { $parser->mOutput->mCacheTime = -1; if ($actions && $groups) $this->directives[] = array($actions,$groups); return ; }
# Process the ifAllowed conditional security directive function ifAllowed(&$parser,$action,$title,$if,$else = ) { return $this->validateTitle($action,Title::newFromText($title)) ? $if : $else; }
# Process the ifGroup conditional security directive # - evaluates to true if current uset belongs to any of the groups in $groups (csv) function ifGroup(&$parser,$groups,$if,$else = ) { global $wgUser; $intersection = array_intersect( array_map('strtolower',split(',',$groups)), array_map('strtolower',$wgUser->getEffectiveGroups()) ); return count($intersection) > 0 ? $if : $else; }
# ArticleAfterFetchContent hook: Asseses security after raw content fetched from database and clears if not readable # - also fills the global $securityCache cache with info to append to the rendered article function onArticleAfterFetchContent(&$article,&$text) { global $wgSecurityDenyTemplate; $title = $article->mTitle->getText(); if (!$this->activeHooks['ArticleAfterFetchContent']) return true; if (!$this->validateTitle('view',$article->mTitle)) $text = '{'.'{'."$wgSecurityDenyTemplate|fetch|$title}}"; return true; }
# Render any security info function onOutputPageBeforeHTML(&$out,&$text) { global $wgUser,$wgTitle,$wgSecurityInfoTemplate,$wgSecurityDenyTemplate, $wgSiteNotice,$wgSecurityParseInfo,$wgSecurityRuleTemplate,$wgSecurityLogActions; static $done = 0; if ($done++) return true; $psr = new Parser; $psropt = ParserOptions::newFromUser($wgUser);
# Add rules information for this article if any $rules = $this->rules[$this->title]; if (count($rules)) {
# Construct wikitext for info $info = '{'.'{'."$wgSecurityInfoTemplate|1=\n"; foreach ($rules as $rule) { $a = $rule[0] == '*' ? 'Every action' : ucfirst($rule[0]); $b = $rule[1] == '*' ? 'anybody' : ($rule[1] == 'user' ? 'logged in' : $rule[1]); $c = isset($rule[2]) ? $rule[2] : ; if ($wgSecurityRuleTemplate) $info .= '{'.'{'."$wgSecurityRuleTemplate|$a|$b|$c}}\n"; else $info .= "*$a requires the user to be $b $c\n"; } $info .= "\n}}";
# Parse the wikitext (depending on $wgSecurityParseInfo) and add to SiteNotice
if ($wgSecurityParseInfo) {
$psrout = $psr->parse($info,$wgTitle,$psropt,true,true);
$info = $psrout->getText();
}
$wgSiteNotice .= $info; }
# Replace main body with deny-message and append log if action allowed if (!$this->allowed) { $text = ; $psrout = $psr->parse('{'.'{'."$wgSecurityDenyTemplate|{$this->action}|{$this->title}}}",$wgTitle,$psropt,true,true); $out->mBodytext = $psrout->getText(); if (in_array($this->action,$wgSecurityLogActions)) { $msg = wfMsgForContent('securitylogdeny',$this->action,$this->title); $log = new LogPage('security',false); $log->addEntry('deny',$wgUser->getUserPage(),$msg); } }
return true; }
# Main validation code for the whole request is done on the first call to userCan function onuserCan(&$title, &$user, $ucaction, &$result) { global $wgTitle,$action,$wgRequest,$wgUser,$wgUploadDirectory, $wgSecurityEnableForImages,$wgSecurityDenyImage,$wgSecurityLogActions;
# Add IsAllowed hook to the $wgUser object static $done = 0; if ($done++ == 0) wfSimpleSecurityAddIsAllowedHook();
# Handle security on files (needs apache mod-rewrite) # - /wiki/images/... rewritten to article title Download/image-path... if (ereg('^Download/(.+)/([^/]+)$',$this->title,$match)) { $this->path = $match[1]; $this->file = $image = $match[2]; if (ereg('^thumb/.+/([^/]+)$',$this->path,$match)) $image = $match[1]; $wgTitle = Title::newFromText($this->title = "Image:$image"); $action = 'raw'; }
# If the requested item is a file, return it to the client if security validates otherwise return the SecurityDenyImage if ($wgSecurityEnableForImages && $this->file) { if (in_array('download',$wgSecurityLogActions)) { $msg = wfMsgForContent('securitylogdeny','download',$this->title); $log = new LogPage('security',false); $log->addEntry('deny',$wgUser->getUserPage(),$msg); } $pathname = $this->allowed ? "$wgUploadDirectory/".$this->path.'/'.$this->file : $wgSecurityDenyImage; while (@ob_end_clean()); header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename="'.$this->file.'"'); $content = implode(,file($pathname)); if (in_array('Content-Encoding: gzip',headers_list())) $content = gzencode($content); echo($content); die; }
return true; }
# Validate the passed database row and replace any invalidate content function validateDatabaseRow(&$row) { global $wgUser,$wgSecuritySysops,$wgSecurityDenyReadTemplate; }
# Validate the passed action against the current user and title function validateAction(&$user,$action) { global $wgSecuritySysops; }
}
- Add IsAllowed hook to the $wgUser object
function wfSimpleSecurityAddIsAllowedHook() { global $wgUser;
# Create a new User class ($User2) by extending the existing one with an overridden isAllowed method $User = get_class($wgUser); $User2 = $User.'2'; eval("class $User2 extends $User".' { function isAllowed($action = "") { global $wgSimpleSecurity; $result = $wgSimpleSecurity->validateAction($this,$action); return $result === NULL ? $result = parent::isAllowed($action) : $result; } }');
# Replace the $wgUser object with an identical $User2 instance $oldUser = $wgUser; $wgUser = new $User2(); foreach (array_keys(get_class_vars($User)) as $k) $wgUser->$k = $oldUser->$k; }
- Add DatabaseFetch and DatabaseQuery hooks
function wfSimpleSecurityAddDatabaseHooks() { global $wgLoadBalancer,$wgDBtype;
wfGetDB(); # This ensures that $wgLoadBalancer is not a stub object when we subclass it
# 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 $type = ucfirst($wgDBtype); eval("class Database{$type}2 extends Database{$type}".' {
public function query($sql,$fname = "",$tempIgnore = false) { $sql = preg_replace_callback("/(?<=SELECT ).+?(?= FROM)/","wfSimpleSecurityPatchSQL",$sql,1); return parent::query($sql,$fname,$tempIgnore); }
function fetchObject(&$res) { global $wgSimpleSecurity; $row = parent::fetchObject($res); $wgSimpleSecurity->validateDatabaseRow($row); return $row; }
}');
# Create a replica of the LoadBalancer class which uses the new Database subclass for its connection objects $LoadBalancer = get_class($wgLoadBalancer); $LoadBalancer2 = $LoadBalancer."2"; eval("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;
wfSimpleSecurityAddDatabaseHooks();
$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.", 'securitylogdeny' => "Attempt to $1 $2 was denied.", 'securitylogentry' => "" )); }
}
- Needed in MediaWiki >1.8.0 for magic word hooks to work properly
function wfSimpleSecurityLanguageGetMagic(&$magicWords,$langCode = 0) { global $wgSecurityMagic,$wgSecurityMagicInherit,$wgSecurityMagicIf,$wgSecurityMagicGroup; $magicWords[$wgSecurityMagic] = array(0,$wgSecurityMagic); $magicWords[$wgSecurityMagicInherit] = array(0,$wgSecurityMagicInherit); $magicWords[$wgSecurityMagicIf] = array(0,$wgSecurityMagicIf); $magicWords[$wgSecurityMagicGroup] = array(0,$wgSecurityMagicGroup); return true; } ?>



