Difference between revisions of "Extension:SimpleSecurity"

From Organic Design wiki
({{svn}})
Line 1: Line 1:
[[Category:Extensions|SimpleSecurity4]]{{voodoo}}
+
[[Category:Extensions|SimpleSecurity]]{{voodoo}}
<php>
+
{{svn|http://svn.wikimedia.org/viewvc/mediawiki/trunk/extensions/SimpleSecurity}}
<?php
 
/**
 
* Simple Security extension
 
* - Extends the MediaWiki article protection to allow restricting viewing of article content
 
* - Also adds #ifusercan and #ifgroup parser functions for rendering restriction-based content
 
*
 
* See http://www.mediawiki.org/Extension:Simple_Security for installation and usage details
 
* See http://www.organicdesign.co.nz/Extension_talk:SimpleSecurity4.php for development notes and disucssion
 
* Version 4.0.0 started 2007-10-11
 
* Version 4.1.0 started 2008-06-12 (development funded for a slimmed down functional version)
 
*
 
* @package MediaWiki
 
* @subpackage Extensions
 
* @author Aran Dunkley [http://www.organicdesign.co.nz/nad User:Nad]
 
* @copyright © 2007 Aran Dunkley
 
* @licence GNU General Public Licence 2.0 or later
 
*/
 
 
 
if (!defined('MEDIAWIKI')) die('Not an entry point.');
 
 
 
define('SIMPLESECURITY_VERSION', '4.1.2, 2008-07-23');
 
 
 
# Global security settings
 
$wgSecurityMagicIf              = "ifusercan";                  # the name for doing a permission-based conditional
 
$wgSecurityMagicGroup          = "ifgroup";                    # the name for doing a group-based conditional
 
$wgSecurityLogActions          = array('edit', 'download');    # Actions that should be logged
 
$wgSecurityUseDBHook            = true;                        # Use the DatabaseFetchHook to validate database access
 
$wgSecurityAllowUser            = false;                        # Allow restrictions based on user not just group
 
$wgSecurityAllowUnreadableLinks = false;                        # Should links to unreadable pages be allowed? (MW1.7+)
 
 
 
# Extra actions to allow control over in protection form
 
$wgSecurityExtraActions  = array(
 
'read'    => 'Read',
 
'source'  => 'Source',
 
'history' => 'History'
 
);
 
$wgSecurityExtraActions  = array('read' => 'Read');
 
 
 
# Extra groups available in protection form
 
$wgSecurityExtraGroups  = array();
 
 
 
array_unshift($wgExtensionFunctions, 'wfSetupSimpleSecurity'); # Put SimpleSecurity's setup function before all others
 
 
 
$wgHooks['LanguageGetMagic'][] = 'wfSimpleSecurityLanguageGetMagic';
 
$wgExtensionCredits['parserhook'][] = array(
 
'name'        => "SimpleSecurity",
 
'author'      => '[http://www.organicdesign.co.nz/User:Nad User:Nad]',
 
'description' => 'Extends the MediaWiki article protection to allow restricting viewing of article content',
 
'url'        => 'http://www.mediawiki.org/wiki/Extension:Simple_Security',
 
'version'    => SIMPLESECURITY_VERSION
 
);
 
 
 
class SimpleSecurity {
 
 
 
var $guid  = '';
 
var $cache = array();
 
 
 
/**
 
* Constructor
 
*/
 
function __construct() {
 
global $wgParser, $wgHooks, $wgLogTypes, $wgLogNames, $wgLogHeaders, $wgLogActions, $wgMessageCache,
 
$wgSecurityMagicIf, $wgSecurityMagicGroup, $wgSecurityExtraActions, $wgSecurityExtraGroups,
 
$wgRestrictionTypes, $wgRestrictionLevels, $wgGroupPermissions, $wgSecurityAllowUnreadableLinks;
 
 
 
# $wgGroupPermissions has to have its default read entry removed because Title::userCanRead checks it directly
 
if ($this->default_read = isset($wgGroupPermissions['*']['read']) && $wgGroupPermissions['*']['read'])
 
$wgGroupPermissions['*']['read'] = false;
 
 
 
# Add our parser-hooks
 
$wgParser->setFunctionHook($wgSecurityMagicIf, array($this, 'ifUserCan'));
 
$wgParser->setFunctionHook($wgSecurityMagicGroup, array($this, 'ifGroup'));
 
$wgHooks['UserGetRights'][] = $this;
 
 
 
# If preventing links to unreadable content, add hook and a unique string
 
if (!$wgSecurityAllowUnreadableLinks) {
 
$wgHooks['GetLocalURL'][] = $this;
 
$this->guid = uniqid('ss4-');
 
}
 
 
 
# Add a new log type
 
$wgLogTypes[]                  = 'security';
 
$wgLogNames  ['security']      = 'securitylogpage';
 
$wgLogHeaders['security']      = 'securitylogpagetext';
 
$wgLogActions['security/deny'] = 'securitylogentry';
 
 
 
# Extend protection form groups, actions and messages
 
$wgMessageCache->addMessages(array('protect-unchain' => "Modify actions individually"));
 
#$wgMessageCache->addMessages(array('loginreqpagetext' => "Sorry, you'll need to $1 to an account with
 
#sufficient permissions to view this page."));
 
foreach ($wgSecurityExtraActions as $k => $v) {
 
if (empty($v)) $v = ucfirst($k);
 
$wgRestrictionTypes[] = $k;
 
$wgMessageCache->addMessages(array( "restriction-$k" => $v ));
 
#$wgGroupPermissions['sysop'][$k] = true; # Ensure sysops have the right to perform this extra action
 
}
 
 
foreach ($wgSecurityExtraGroups as $k => $v) {
 
if (empty($v)) $v = ucfirst($k);
 
$wgRestrictionLevels[] = $k;
 
$wgMessageCache->addMessages(array( "protect-level-$k" => $v ));
 
$wgGroupPermissions[$k]['not an action'] = true; # Ensure the new groups show up in rights management
 
}
 
}
 
 
 
/**
 
* Process the ifUserCan conditional security directive
 
*/
 
public function ifUserCan(&$parser, $action, $title, $then, $else = '') {
 
return $title->userCan($action) ? $then : $else;
 
}
 
 
 
/**
 
* Process the ifGroup conditional security directive
 
* - evaluates to true if current uset belongs to any of the comma-separated users and/or groups in the first parameter
 
*/
 
public function ifGroup(&$parser, $groups, $then, $else = '') {
 
global $wgUser;
 
$intersection = array_intersect(array_map('strtolower', split(',', $groups)), $wgUser->getEffectiveGroups());
 
return count($intersection) > 0 ? $then : $else;
 
}
 
 
 
/**
 
* Make links to unreadable pages into a guid for an url so they can be converted to not a link later
 
*/
 
public function onGetLocalURL(&$title, &$url, &$query) {
 
global $wgUser, $wgHooks;
 
if (!$this->validateTitle($wgUser, $title, $error)) {
 
$url = $this->guid;
 
static $hookAdded = 0;
 
if ($hookAdded++ == 0) $wgHooks['BeforePageDisplay'][] = $this;
 
}
 
return true;
 
}
 
 
 
/**
 
* Convert the urls with guids for hrefs into non-clickable text of class "unreadable"
 
*/
 
public function onBeforePageDisplay(&$out) {
 
$out->mBodytext = preg_replace(
 
"|<a href=\"{$this->guid}\".+?>(.+?)</a>|",
 
"<span class=\"unreadable\">$1</span>",
 
$out->mBodytext
 
);
 
return true;
 
}
 
 
 
/**
 
* User::getRights returns a list of rights (allowed actions) based on the current users group membership
 
* Title::getRestrictions returns a list of groups who can perform a particular action
 
* So getRights should filter out any title-based restriction's actions which require groups that the user is not a member of
 
* Allows sysop access
 
*/
 
public function onUserGetRights(&$user, &$rights, $title = NULL) {
 
global $wgGroupPermissions, $wgTitle, $wgRequest;
 
 
 
if (!is_object($title)) $title = $wgTitle;
 
$groups = $user->getEffectiveGroups();
 
 
 
# If no title, or user is sysop, bail out now
 
if (!is_object($title) || in_array('sysop', $groups)) return true;
 
 
 
# Hack to prevent specialpage operations on unreadable pages
 
$ns = $title->getNamespace();
 
if ($ns == NS_SPECIAL) {
 
list($name, $par) = explode('/', $title->getDBkey(), 2);
 
if ($par) $title  = Title::newFromText($par);
 
elseif ($wgRequest->getVal('target'))  $title = Title::newFromText($wgRequest->getVal('target'));
 
elseif ($wgRequest->getVal('oldtitle')) $title = Title::newFromText($wgRequest->getVal('oldtitle'));
 
}
 
 
 
# If title is not readable by user, remove the read and move rights
 
if (!$this->validateTitle($user, $title, $error))
 
foreach ($rights as $i => $right) if ($right == 'read' || $right == 'move') unset($rights[$i]);
 
 
 
# See constructor for details of this
 
elseif ($this->default_read) $wgGroupPermissions['*']['read'] = $this->default_read;
 
 
 
return true;
 
}
 
 
/**
 
* Patches SQL queries to ensure that the old_id field is present in all requests for the old_text field
 
* otherwise the title that the old_text is associated with can't be determined
 
*/
 
static function patchSQL($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";
 
}
 
 
 
/**
 
* Validate the passed database row and replace any invalid content
 
* - called from DatabaseFetchHook whenever a row contains old_text
 
* - old_id is guaranteed to exist due to patchSQL method
 
*/
 
static function validateRow(&$row) {
 
global $wgUser, $wgSimpleSecurity;
 
$groups = $wgUser->getEffectiveGroups();
 
if (in_array('sysop', $groups)) return;
 
 
 
# Obtain a title object from the old_id
 
$dbr  =& wfGetDB(DB_SLAVE);
 
$tbl  = $dbr->tableName('revision');
 
$rev  = $dbr->selectRow($tbl, 'rev_page', "rev_text_id = {$row->old_id}", __METHOD__);
 
$title = Title::newFromID($rev->rev_page);
 
 
 
# Replace text content in the passed database row if title unreadable by user
 
if (!$wgSimpleSecurity->validateTitle($wgUser, $title, $error)) $row->old_text = $error;
 
}
 
 
 
/**
 
* Return bool for whether or not a title can be read by user
 
* - if there are read restrictions in place for the title, check if user a member of any groups required for read access
 
*/
 
public function validateTitle(&$user, &$title, &$error) {
 
$groups = $user->getEffectiveGroups();
 
if (!is_object($title) || in_array('sysop', $groups)) return true;
 
 
 
# Cache results
 
$key = $user->getID().'\x07'.$title->getPrefixedText();
 
if (array_key_exists($key, $this->cache)) {
 
$error = $this->cache[$key][1];
 
return $this->cache[$key][0];
 
}
 
 
# Determine whether valid and create error message if not
 
$restrictions = $title->getRestrictions('read');
 
if ($valid = (count($restrictions) < 1 || count(array_intersect($restrictions, $groups)) > 0)) $error = '';
 
else {
 
$restrictions = array_map('ucfirst', $restrictions);
 
$groups = array_pop($restrictions);
 
if (count($restrictions) > 0) $groups = 'groups '.join(', ', $restrictions)." and $groups";
 
else $groups = "the $groups group";
 
$error = wfMsg('badaccess-read', $title->getPrefixedText(), $groups);
 
}
 
 
$this->cache[$key] = array($valid, $error);
 
return $valid;
 
}
 
 
 
/**
 
* Needed in some versions to prevent Special:Version from breaking
 
*/
 
public function __toString() {
 
return __CLASS__;
 
}
 
}
 
 
 
/**
 
* Hooks into Database::query and Database::fetchObject via the LoadBalancer class
 
* - this is a global because PHP doesn't like nested class definitions
 
*/
 
function wfAddDatabaseHooks() {
 
global $wgLoadBalancer, $wgDBtype;
 
 
 
# This ensures that $wgLoadBalancer is not a stub object when we subclass it
 
# todo: this should be able to work in the case of it being a stub object
 
wfGetDB();
 
 
 
# 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
 
# - the changes to this class are only active for SELECT statements and while not processing security directives
 
$type = ucfirst($wgDBtype);
 
eval("class Database{$type}2 extends Database{$type}".' {
 
public function query($sql, $fname = "", $tempIgnore = false) {
 
$count = false;
 
$patched = preg_replace_callback("/(?<=SELECT ).+?(?= FROM)/", "SimpleSecurity::patchSQL", $sql, 1, $count);
 
return parent::query($count ? $patched : $sql, $fname, $tempIgnore);
 
}
 
function fetchObject(&$res) {
 
$row = parent::fetchObject($res);
 
if (isset($row->old_text)) SimpleSecurity::validateRow($row);
 
return $row;
 
}
 
}');
 
 
 
# Create a replica of the LoadBalancer class which uses the new Database subclass for its connection objects
 
class LoadBalancer2 extends LoadBalancer {
 
function reallyOpenConnection(&$server) {
 
$server['type'] .= '2';
 
return parent::reallyOpenConnection($server);
 
}
 
}
 
 
 
# 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;
 
}
 
 
 
/**
 
* Called from $wgExtensionFunctions array when initialising extensions
 
*/
 
function wfSetupSimpleSecurity() {
 
global $wgSimpleSecurity, $wgLanguageCode, $wgMessageCache, $wgSecurityUseDBHook;
 
 
 
# Hooks into Database::query and Database::fetchObject via the LoadBalancer class
 
if ($wgSecurityUseDBHook) wfAddDatabaseHooks();
 
 
 
# Instantiate the SimpleSecurity singleton now that the environment is prepared
 
$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]].",
 
'securitylogentry'    => "",
 
'badaccess-read'      => "\nCannot display any content from \"$1\", it is only readable by $2.\n"
 
));
 
}
 
}
 
 
 
/**
 
* Register magic words
 
*/
 
function wfSimpleSecurityLanguageGetMagic(&$magicWords, $langCode = 0) {
 
global $wgSecurityMagicIf, $wgSecurityMagicGroup;
 
$magicWords[$wgSecurityMagicIf]    = array($langCode, $wgSecurityMagicIf);
 
$magicWords[$wgSecurityMagicGroup] = array($langCode, $wgSecurityMagicGroup);
 
return true;
 
}
 
</php>
 

Revision as of 09:20, 24 July 2008

Voodoo.svg This code exhibits voodoo programming techniques. The most common of these is extending an instance's class at runtime after it has been instantiated, a technique that can be used to provide additional hooks into existing code without requiring modification of code-base files. For a list of all our scripts which exhibit voodoo, see Category:Code that uses voodoo.
Info.svg This code is in our Git repository here.

Note: If there is no information in this page about this code and it's a MediaWiki extension, there may be something at mediawiki.org.