Extension:SimpleSecurity3.php
<php> <?php
- Simple security extension
- - 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
- - The following directives allows everyone view-only access to this article
if (!defined('MEDIAWIKI')) die('Not an entry point.');
define('SIMPLESECURITY_VERSION','3.4.8, 2007-07-25');
- Global security settings
$wgSecurityMagic = "security"; # the parser-function name for security directives $wgSecurityMagicNoi = "!security"; # the name for non-inheriting security directives $wgSecurityMagicIf = "ifusercan"; # the name for doing a permission-based conditional $wgSecurityMagicGroup = "ifgroup"; # the name for doing a group-based conditional $wgSecurityEnableInheritance = false; # specifies whether or not security directives in categories inherit to member articles $wgSecurityEnableForImages = false; # specifies whether security directives in image/file articles also apply to the associated binary $wgSecuritySysops = array('sysop'); # the list of groups whose members bypass all security (groups a all lowercase, user are ucfirst) $wgSecurityDenyTemplate = 'Template:Action not permitted'; $wgSecurityInfoTemplate = 'Template:Security info'; $wgSecurityRuleTemplate = ; # set to a template for the $wgSecurityDenyImage = "$IP/skins/common/images/mediawiki.png"; # the image returned in place of requested image if read access denied $wgSecurityParseInfo = isset($wgSecurityParseInfo) ? $wgSecurityParseInfo : false; $wgSecurityGroupsArticle = ; # Name of an article which contains a bullet list of available groups for Special:Userrights $wgSecurityLogActions = array('download'); # Actions that should be logged
$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 {
# Private internal data var $initialised; var $rules; var $directives; var $activeHooks; var $title; var $action; var $allowed; var $file; var $path;
# Needed in some versions to prevent Special:Version from breaking function __toString() { return 'SimpleSecurity'; }
# Constructor function SimpleSecurity($inheritance = true) { global $wgParser,$wgHooks,$wgLogTypes,$wgLogNames,$wgLogHeaders,$wgLogActions, $wgSecurityMagic,$wgSecurityMagicNoi,$wgSecurityMagicIf,$wgSecurityMagicGroup, $wgSecurityEnableInheritance,$wgGroupPermissions,$wgSecurityGroupsArticle;
# Add all our required event hooks $wgHooks['userCan'][] = $this; $wgHooks['ArticleAfterFetchContent'][] = $this; $wgHooks['OutputPageBeforeHTML'][] = $this; $wgHooks['DatabaseFetchObject'][] = $this;
$wgParser->setFunctionHook($wgSecurityMagic,array($this,'processDirective')); if ($wgSecurityEnableInheritance) $wgParser->setFunctionHook($wgSecurityMagicNoi,array($this,'processDirectiveNoi')); $wgParser->setFunctionHook($wgSecurityMagicIf,array($this,'ifUserCan')); $wgParser->setFunctionHook($wgSecurityMagicGroup,array($this,'ifGroup'));
# Specify which of the hooks are currently active because these apply to all parser objects $this->activeHooks = array( 'ArticleAfterFetchContent' => false, 'ParserBeforeStrip' => false );
# Initialise internal data $this->initialised = false; $this->allowed = true; $this->directives = array(); $this->rules = array(); $this->file = ; $this->path = ;
# Add extra available groups if $wgSecurityGroupsArticle is set if ($wgSecurityGroupsArticle) { $groups = new Article(Title::newFromText($wgSecurityGroupsArticle)); if (preg_match_all('/^\\*\\s*(.+?)\\s*$/m',$groups->getContent(),$match)) foreach($match[1] as $group) $wgGroupPermissions[$group] = array(); }
# 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 = ) { if ($actions && $groups) $this->directives[] = array($actions,$groups); return ; }
# Process a non-inheriting security directive function processDirectiveNoi(&$parser,$actions = ,$groups = ) { if ($actions && $groups) $this->directives[] = array($actions,$groups); return ; }
# Process the ifUserCan conditional security directive function ifUserCan(&$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; }
# This is an experimental hook made available by Extension:DatabaseFetchObject to allow security validation on database row access function onDatabaseFetchObject(&$db,&$row) { return true; }
# 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;
$this->title = $wgTitle->getPrefixedURL(); $this->action = $action == 'submit' ? 'edit' : strtolower($action);
# Moves need to be handled differently if ($this->title == 'Special:Movepage' && $this->action == 'submit') { $this->action = 'move'; $this->title = $wgRequest->getText('wpOldTitle',$wgRequest->getVal('target')); }
# 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 ($this->initialised) { # we can call $this->validate in here so mediawiki can render links according to permissions } elseif (!empty($wgUser->mDataLoaded)) {
# Activate the hooks now that enough information is present to assess security $this->initialised = true; $this->activeHooks['ArticleAfterFetchContent'] = true;
# Validate current global action and set to view if not allowed if (!$this->allowed = $this->validateTitle($this->action,$wgTitle)) { $action = 'view'; global $wgEnableParserCache,$wgOut; $wgEnableParserCache = false; $wgOut->enableClientCache(false); }
# 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; }
# Return whether or not the passed action is permitted on the passed title # - uses $this->rules[title] as a cache function validateTitle($action,$title) { global $extramsg,$wgUser,$wgSecurityMagic,$wgSecurityMagicNoi,$wgSecurityEnableInheritance; $key = $title->getPrefixedURL(); if (isset($this->rules[$key])) $rules = $this->rules[$key]; else { # Disable the AfterDatabaseFetch hook to avoid an infinite loop and get the article text $this->activeHooks['ArticleAfterFetchContent'] = false; $this->activeHooks['ParserBeforeStrip'] = false; $article = new Article($title); $text = $article->getContent();
# Set up a new local parser object to process security directives independently of main rendering process $psr = new Parser; $psr->setFunctionHook($wgSecurityMagic,array($this,'processDirective')); if ($wgSecurityEnableInheritance) $psr->setFunctionHook($wgSecurityMagicNoi,array($this,'processDirectiveNoi')); $opt = ParserOptions::newFromUser($wgUser); $this->directives = array(); $out = $psr->parse($text,$title,$opt,false,true); $rules = $this->directives;
# Get the security items from the cats by running the parser over the content of each # - stop checking MagicNoi directives because they shouldn't inherit if ($wgSecurityEnableInheritance) { unset($psr->mFunctionHooks[$wgSecurityMagicNoi]); foreach ($out->getCategoryLinks() as $cat) { $ca = new Article($ct = Title::newFromText($cat = "Category:$cat")); $this->directives = array(); $psr->parse($ca->getContent(),$ct,$opt,false,true); foreach ($this->directives as $i) $rules[] = array($i[0],$i[1],"this rule is inherited from $cat"); } }
# Re-enable AfterFetch hook and cache the security info $this->activeHooks['ArticleAfterFetchContent'] = true; $this->activeHooks['ParserBeforeStrip'] = true; $this->rules[$key] = $rules; }
# Return the result of validating the extracted rules return $this->validateRules($action,$rules); }
# Return whether or not a user is allowed to perform an action according to an array of security items function validateRules($action,&$rules) { global $wgUser,$wgSecuritySysops; if (!is_array($rules)) return true;
# Resolve permission for this action from the extracted security links $security = ; foreach ($rules as $i) { #if ($i[1] == ) $i[1] = join(',',$wgSecuritySysops); $actions = preg_split("/\\s*,\\s*/",strtolower($i[0])); if (in_array($action,$actions) or (in_array('*',$actions) and $security == )) $security = $i[1]; }
# Get users group lists (add own username to groups) $groups = array_map('strtolower',$wgUser->getEffectiveGroups()); $groups[] = ucfirst($wgUser->mName); $security = $security ? preg_split("/\\s*,\\s*/",$security) : array();
# Calculate whether or not the action can be performed and return the result return ( count($security) == 0 or in_array('*',$security) or count(array_intersect($groups,$wgSecuritySysops)) > 0 or count(array_intersect($groups,$security)) > 0 ); } }
- Called from $wgExtensionFunctions array when initialising extensions
function wfSetupSimpleSecurity() { global $wgSimpleSecurity,$wgLanguageCode,$wgMessageCache;
$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,$wgSecurityMagicNoi,$wgSecurityMagicIf,$wgSecurityMagicGroup,$wgSecurityEnableInheritance; $magicWords[$wgSecurityMagic] = array(0,$wgSecurityMagic); if ($wgSecurityEnableInheritance) $magicWords[$wgSecurityMagicNoi] = array(0,$wgSecurityMagicNoi); $magicWords[$wgSecurityMagicIf] = array(0,$wgSecurityMagicIf); $magicWords[$wgSecurityMagicGroup] = array(0,$wgSecurityMagicGroup); return true; } </php>