Personal tools


Extension:SimpleSecurity.php

From OrganicDesign

(Redirected from SimpleSecurity.php)
Jump to: navigation, search
<?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'      => '[http://www.organicdesign.co.nz/User:Nad 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 [[MW:Extension:SimpleSecurity|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;
	}
 
?>