From OrganicDesign
(Redirected from
SimpleSecurity.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' => '[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;
}
?>