Extension:SimpleSecurity3.php

From Organic Design wiki
Revision as of 06:54, 24 July 2008 by Nad (talk | contribs) (New page: SimpleSecurity...)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Legacy.svg Legacy: This article describes a concept that has been superseded in the course of ongoing development on the Organic Design wiki. Please do not develop this any further or base work on this concept, this is only useful for a historic record of work done. You may find a link to the currently used concept or function in this article, if not you can contact the author to find out what has taken the place of this legacy item.

Please refer to [[Simple Security 4]] instead.

<php> <?php

  1. Simple security extension
  2. - See http://www.mediawiki.org/Extension:Simple_Security for installation and usage details
  3. - Licenced under LGPL (http://www.gnu.org/copyleft/lesser.html)
  4. - Needs apache's mod-rewrite for security on images, see code comments below
  5. - 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');

  1. 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 ); } }


  1. 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' => "" )); }

}

  1. 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>