r48859 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r48858‎ | r48859 | r48860 >
Date:05:23, 26 March 2009
Author:nad
Status:deferred
Tags:
Comment:
Split code out to separate class and i18n files
Modified paths:
  • /trunk/extensions/SimpleSecurity/SimpleSecurity.i18n.php (added) (history)
  • /trunk/extensions/SimpleSecurity/SimpleSecurity_body.php (added) (history)

Diff [purge]

Index: trunk/extensions/SimpleSecurity/SimpleSecurity_body.php
@@ -0,0 +1,341 @@
 2+<?php
 3+/**
 4+ * SimpleSecurity class
 5+ */
 6+class SimpleSecurity {
 7+
 8+ var $guid = '';
 9+ var $cache = array();
 10+ var $info = array();
 11+
 12+ function __construct() {
 13+ global $wgParser, $wgHooks, $wgLogTypes, $wgLogNames, $wgLogHeaders, $wgLogActions, $wgMessageCache,
 14+ $wgSecurityMagicIf, $wgSecurityMagicGroup, $wgSecurityExtraActions, $wgSecurityExtraGroups,
 15+ $wgRestrictionTypes, $wgRestrictionLevels, $wgGroupPermissions,
 16+ $wgSecurityRenderInfo, $wgSecurityAllowUnreadableLinks;
 17+
 18+ # $wgGroupPermissions has to have its default read entry removed because Title::userCanRead checks it directly
 19+ if ( $this->default_read = ( isset( $wgGroupPermissions['*']['read'] ) && $wgGroupPermissions['*']['read'] ) )
 20+ $wgGroupPermissions['*']['read'] = false;
 21+
 22+ # Add our hooks
 23+ $wgHooks['UserGetRights'][] = $this;
 24+ if ( $wgSecurityMagicIf ) $wgParser->setFunctionHook( $wgSecurityMagicIf, array( $this, 'ifUserCan' ) );
 25+ if ( $wgSecurityMagicGroup ) $wgParser->setFunctionHook( $wgSecurityMagicGroup, array( $this, 'ifGroup' ) );
 26+ if ( $wgSecurityAllowUnreadableLinks ) $wgHooks['BeforePageDisplay'][] = $this;
 27+ if ( $wgSecurityRenderInfo ) $wgHooks['OutputPageBeforeHTML'][] = $this;
 28+
 29+ # Add a new log type
 30+ $wgLogTypes[] = 'security';
 31+ $wgLogNames ['security'] = 'securitylogpage';
 32+ $wgLogHeaders['security'] = 'securitylogpagetext';
 33+ $wgLogActions['security/deny'] = 'securitylogentry';
 34+
 35+ # Load messages
 36+ wfLoadExtensionMessages ( 'SimpleSecurity' );
 37+ $wgMessageCache->addMessages( array( 'badaccess-group1' => wfMsg( 'badaccess-group0' ) ) );
 38+ $wgMessageCache->addMessages( array( 'badaccess-group2' => wfMsg( 'badaccess-group0' ) ) );
 39+ $wgMessageCache->addMessages( array( 'badaccess-groups' => wfMsg( 'badaccess-group0' ) ) );
 40+
 41+ foreach ( $wgSecurityExtraActions as $k => $v ) {
 42+ if ( empty( $v ) ) $v = ucfirst( $k );
 43+ $wgRestrictionTypes[] = $k;
 44+ $wgMessageCache->addMessages( array( "restriction-$k" => $v ) );
 45+ }
 46+
 47+ # Ensure the new groups show up in rights management
 48+ # - note that 1.13 does a strange check in the ProtectionForm::buildSelector
 49+ # $wgUser->isAllowed($key) where $key is an item from $wgRestrictionLevels
 50+ # this requires that we treat the extra groups as an action and make sure its allowed by the user
 51+ foreach ( $wgSecurityExtraGroups as $k => $v ) {
 52+ if ( empty( $v ) ) $v = ucfirst( $k );
 53+ $wgRestrictionLevels[] = $k;
 54+ $wgMessageCache->addMessages( array( "protect-level-$k" => $v ) );
 55+ $wgGroupPermissions[$k][$k] = true; # members of $k must be allowed to perform $k
 56+ $wgGroupPermissions['sysop'][$k] = true; # sysops must be allowed to perform $k as well
 57+ }
 58+ }
 59+
 60+ /**
 61+ * Process the ifUserCan conditional security directive
 62+ */
 63+ public function ifUserCan( &$parser, $action, $pagename, $then, $else = '' ) {
 64+ return Title::newFromText( $pagename )->userCan( $action ) ? $then : $else;
 65+ }
 66+
 67+ /**
 68+ * Process the ifGroup conditional security directive
 69+ * - evaluates to true if current uset belongs to any of the comma-separated users and/or groups in the first parameter
 70+ */
 71+ public function ifGroup( &$parser, $groups, $then, $else = '' ) {
 72+ global $wgUser;
 73+ $intersection = array_intersect( array_map( 'strtolower', split( ',', $groups ) ), $wgUser->getEffectiveGroups() );
 74+ return count( $intersection ) > 0 ? $then : $else;
 75+ }
 76+
 77+ /**
 78+ * Convert the urls with guids for hrefs into non-clickable text of class "unreadable"
 79+ */
 80+ public function onBeforePageDisplay( &$out ) {
 81+ $out->mBodytext = preg_replace_callback(
 82+ "|<a[^>]+title=\"(.+?)\".+?>(.+?)</a>|",
 83+ array( $this, 'unreadableLink' ),
 84+ $out->mBodytext
 85+ );
 86+ return true;
 87+ }
 88+
 89+ /**
 90+ * Render security info if any restrictions on this title
 91+ */
 92+ public function onOutputPageBeforeHTML( &$out, &$text ) {
 93+ global $wgTitle, $wgUser;
 94+
 95+ # Render security info if any
 96+ if ( is_object( $wgTitle ) && $wgTitle->exists() && count( $this->info['LS'] ) + count( $this->info['PR'] ) ) {
 97+
 98+ $rights = $wgUser->getRights();
 99+ $wgTitle->getRestrictions( false );
 100+ $reqgroups = $wgTitle->mRestrictions;
 101+ $sysop = in_array( 'sysop', $wgUser->getGroups() );
 102+
 103+ # Build restrictions text
 104+ $itext = "<ul>\n";
 105+ foreach ( $this->info as $source => $rules ) if ( !( $sysop && $source === 'CR' ) ) {
 106+ foreach ( $rules as $info ) {
 107+ list( $action, $groups, $comment ) = $info;
 108+ $gtext = $this->groupText( $groups );
 109+ $itext .= "<li>" . wfMsg( 'security-inforestrict', "<b>$action</b>", $gtext ) . " $comment</li>\n";
 110+ }
 111+ }
 112+ if ( $sysop ) $itext .= "<li>" . wfMsg( 'security-infosysops' ) . "</li>\n";
 113+ $itext .= "</ul>\n";
 114+
 115+ # Add some javascript to allow toggling the security-info
 116+ $out->addScript( "<script type='text/javascript'>
 117+ function toggleSecurityInfo() {
 118+ var info = document.getElementById('security-info');
 119+ info.style.display = info.style.display ? '' : 'none';
 120+ }</script>"
 121+ );
 122+
 123+ # Add info-toggle before title and hidden info after title
 124+ $link = "<a href='javascript:'>" . wfMsg( 'security-info-toggle' ) . "</a>";
 125+ $link = "<span onClick='toggleSecurityInfo()'>$link</span>";
 126+ $info = "<div id='security-info-toggle'>" . wfMsg( 'security-info', $link ) . "</div>\n";
 127+ $text = "$info<div id='security-info' style='display:none'>$itext</div>\n$text";
 128+ }
 129+
 130+ return true;
 131+ }
 132+
 133+ /**
 134+ * Callback function for unreadable link replacement
 135+ */
 136+ private function unreadableLink( $match ) {
 137+ global $wgUser;
 138+ return $this->userCanReadTitle( $wgUser, Title::newFromText( $match[1] ), $error )
 139+ ? $match[0] : "<span class=\"unreadable\">$match[2]</span>";
 140+ }
 141+
 142+ /**
 143+ * User::getRights returns a list of rights (allowed actions) based on the current users group membership
 144+ * Title::getRestrictions returns a list of groups who can perform a particular action
 145+ * So getRights should filter out any title-based restriction's actions which require groups that the user is not a member of
 146+ * - Allows sysop access
 147+ * - clears and populates the info array
 148+ */
 149+ public function onUserGetRights( &$user, &$rights ) {
 150+ global $wgGroupPermissions, $wgTitle, $wgRequest, $wgPageRestrictions;
 151+
 152+ # Hack to prevent specialpage operations on unreadable pages
 153+ if ( !is_object( $wgTitle ) ) return true;
 154+ $title = $wgTitle;
 155+ $ns = $title->getNamespace();
 156+ if ( $ns == NS_SPECIAL ) {
 157+ list( $name, $par ) = explode( '/', $title->getDBkey() . '/', 2 );
 158+ if ( $par ) $title = Title::newFromText( $par);
 159+ elseif ( $wgRequest->getVal( 'target' ) ) $title = Title::newFromText( $wgRequest->getVal( 'target' ) );
 160+ elseif ( $wgRequest->getVal( 'oldtitle' ) ) $title = Title::newFromText( $wgRequest->getVal( 'oldtitle' ) );
 161+ }
 162+ if ( !is_object( $title ) ) return true; # If still no usable title bail
 163+
 164+ $this->info['LS'] = array(); # security info for rules from LocalSettings ($wgPageRestrictions)
 165+ $this->info['PR'] = array(); # security info for rules from protect tab
 166+ $this->info['CR'] = array(); # security info for rules which are currently in effect
 167+ $groups = $user->getEffectiveGroups();
 168+
 169+ # Put the anon read right back in $wgGroupPermissions if it was there initially
 170+ # - it had to be removed because Title::userCanRead short-circuits with it
 171+ if ( $this->default_read ) {
 172+ $wgGroupPermissions['*']['read'] = true;
 173+ $rights[] = 'read';
 174+ }
 175+
 176+ # Filter rights according to $wgPageRestrictions
 177+ # - also update LS (rules from local settings) items to info array
 178+ $this->pageRestrictions( $rights, $groups, $title, true );
 179+
 180+ # Add PR (rules from article's protect tab) items to info array
 181+ # - allows rules in protection tab to override those from $wgPageRestrictions
 182+ if ( !$title->mRestrictionsLoaded ) $title->loadRestrictions();
 183+ foreach ( $title->mRestrictions as $a => $g ) if ( count( $g ) ) {
 184+ $this->info['PR'][] = array( $a, $g, wfMsg( 'security-desc-PR' ) );
 185+ if ( array_intersect( $groups, $g ) ) $rights[] = $a;
 186+ }
 187+
 188+ # If title is not readable by user, remove the read and move rights
 189+ if ( !in_array( 'sysop', $groups ) && !$this->userCanReadTitle( $user, $title, $error ) ) {
 190+ foreach ( $rights as $i => $right ) if ( $right === 'read' || $right === 'move' ) unset( $rights[$i] );
 191+ #$this->info['CR'] = array('read', '', '');
 192+ }
 193+
 194+ return true;
 195+ }
 196+
 197+ /**
 198+ * Patches SQL queries to ensure that the old_id field is present in all requests for the old_text field
 199+ * otherwise the title that the old_text is associated with can't be determined
 200+ */
 201+ static function patchSQL( $match ) {
 202+ if ( !preg_match( "/old_text/", $match[0] ) ) return $match[0];
 203+ $fields = str_replace( " ", "", $match[0] );
 204+ return ( $fields == "*" || preg_match( "/old_id/", $fields ) ) ? $fields : "$fields,old_id";
 205+ }
 206+
 207+ /**
 208+ * Validate the passed database row and replace any invalid content
 209+ * - called from fetchObject hook whenever a row contains old_text
 210+ * - old_id is guaranteed to exist due to patchSQL method
 211+ * - bails if sysop
 212+ */
 213+ public function validateRow( &$row ) {
 214+ global $wgUser;
 215+ $groups = $wgUser->getEffectiveGroups();
 216+ if ( in_array( 'sysop', $groups ) || empty( $row->old_id ) ) return;
 217+
 218+ # Obtain a title object from the old_id
 219+ $dbr = wfGetDB( DB_SLAVE );
 220+ $tbl = $dbr->tableName( 'revision' );
 221+ $rev = $dbr->selectRow( $tbl, 'rev_page', "rev_text_id = {$row->old_id}", __METHOD__ );
 222+ $title = Title::newFromID( $rev->rev_page );
 223+
 224+ # Replace text content in the passed database row if title unreadable by user
 225+ if ( !$this->userCanReadTitle( $wgUser, $title, $error ) ) $row->old_text = $error;
 226+ }
 227+
 228+ /**
 229+ * Return bool for whether or not passed user has read access to the passed title
 230+ * - if there are read restrictions in place for the title, check if user a member of any groups required for read access
 231+ */
 232+ public function userCanReadTitle( &$user, &$title, &$error ) {
 233+ $groups = $user->getEffectiveGroups();
 234+ if ( !is_object( $title ) || in_array( 'sysop', $groups ) ) return true;
 235+
 236+ # Retrieve result from cache if exists (for re-use within current request)
 237+ $key = $user->getID().'\x07'.$title->getPrefixedText();
 238+ if (array_key_exists($key, $this->cache)) {
 239+ $error = $this->cache[$key][1];
 240+ return $this->cache[$key][0];
 241+ }
 242+
 243+ # Determine readability based on $wgPageRestrictions
 244+ $rights = array( 'read' );
 245+ $this->pageRestrictions( $rights, $groups, $title );
 246+ $readable = count( $rights ) > 0;
 247+
 248+ # If there are title restrictions that prevent reading, they override $wgPageRestrictions readability
 249+ $whitelist = $title->getRestrictions( 'read' );
 250+ if ( count( $whitelist ) > 0 && !count( array_intersect( $whitelist, $groups ) ) > 0 ) $readable = false;
 251+
 252+ $error = $readable ? "" : wfMsg( 'badaccess-read', $title->getPrefixedText() );
 253+ $this->cache[$key] = array( $readable, $error );
 254+ return $readable;
 255+ }
 256+
 257+ /**
 258+ * Returns a textual description of the passed list
 259+ */
 260+ private function groupText( &$groups ) {
 261+ $gl = $groups;
 262+ $gt = array_pop( $gl );
 263+ if ( count( $groups ) > 1 ) $gt = wfMsg( 'security-manygroups', "<b>" . join( "</b>, <b>", $gl ) . "</b>", "<b>$gt</b>" );
 264+ else $gt = "the <b>$gt</b> group";
 265+ return $gt;
 266+ }
 267+
 268+ /**
 269+ * Reduce the passed list of rights based on $wgPageRestrictions and the passed groups and title
 270+ * $wgPageRestrictions contains category and namespace based permissions rules
 271+ * the format of the rules is [type][action] = group(s)
 272+ * also adds LS items and currently active LS to info array
 273+ */
 274+ private function pageRestrictions( &$rights, &$groups, &$title, $updateInfo = false ) {
 275+ global $wgPageRestrictions;
 276+ $cats = array();
 277+ foreach ( $wgPageRestrictions as $k => $restriction ) if ( preg_match( '/^(.+?):(.*)$/', $k, $m ) ) {
 278+ $type = ucfirst( $m[1] );
 279+ $data = $m[2];
 280+ $deny = false;
 281+
 282+ # Validate rule against the title based on its type
 283+ switch ($type) {
 284+
 285+ case "Category":
 286+
 287+ # If processing first category rule, build a list of cats this article belongs to
 288+ if ( count( $cats ) == 0 ) {
 289+ $dbr = wfGetDB( DB_SLAVE );
 290+ $cl = $dbr->tableName( 'categorylinks' );
 291+ $id = $title->getArticleID();
 292+ $res = $dbr->select( $cl, 'cl_to', "cl_from = '$id'", __METHOD__, array( 'ORDER BY' => 'cl_sortkey' ) );
 293+ while ( $row = $dbr->fetchRow( $res ) ) $cats[] = $row[0];
 294+ $dbr->freeResult( $res );
 295+ }
 296+
 297+ $deny = in_array( $data, $cats );
 298+ break;
 299+
 300+ case "Namespace":
 301+ $deny = $data == $title->getNsText();
 302+ break;
 303+ }
 304+
 305+ # If the rule applies to this title, check if we're a member of the required groups,
 306+ # remove action from rights list if not (can be mulitple occurences)
 307+ # - also update info array with page-restriction that apply to this title (LS), and rules in effect for this user (CR)
 308+ if ( $deny ) {
 309+ foreach ( $restriction as $action => $reqgroups ) {
 310+ if ( !is_array( $reqgroups ) ) $reqgroups = array( $reqgroups );
 311+ if ( $updateInfo ) $this->info['LS'][] = array( $action, $reqgroups, wfMsg( 'security-desc-LS', strtolower( $type ), $data ) );
 312+ if ( !in_array( 'sysop', $groups ) && !array_intersect( $groups, $reqgroups ) ) {
 313+ foreach ( $rights as $i => $right ) if ( $right === $action ) unset( $rights[$i] );
 314+ #$this->info['CR'][] = array($action, $reqgroups, wfMsg('security-desc-CR'));
 315+ }
 316+ }
 317+ }
 318+ }
 319+ }
 320+
 321+ /**
 322+ * Updates passed LoadBalancer's DB servers to secure class
 323+ */
 324+ static function updateLB( &$lb ) {
 325+ $lb->closeAll();
 326+ foreach ( $lb->mServers as $i => $server ) $lb->mServers[$i]['type'] = 'SimpleSecurity';
 327+ }
 328+
 329+ /**
 330+ * Hack to ensure proper search class is used
 331+ * - $wgDBtype determines search class unless already defined in $wgSearchType
 332+ * - just copied method from SearchEngine::create()
 333+ */
 334+ static function fixSearchType() {
 335+ global $wgDBtype, $wgSearchType;
 336+ if ( $wgSearchType ) return;
 337+ elseif ( $wgDBtype == 'mysql' ) $wgSearchType = 'SearchMySQL4';
 338+ elseif ( $wgDBtype == 'postgres' ) $wgSearchType = 'SearchPostgres';
 339+ elseif ( $wgDBtype == 'oracle' ) $wgSearchType = 'SearchOracle';
 340+ else $wgSearchType = 'SearchEngineDummy';
 341+ }
 342+}
Index: trunk/extensions/SimpleSecurity/SimpleSecurity.i18n.php
@@ -0,0 +1,30 @@
 2+<?php
 3+/**
 4+ * Internationalisation for SimpleSecurity extension
 5+ *
 6+ * @author Nad
 7+ * @file
 8+ * @ingroup Extensions
 9+ */
 10+
 11+$messages = array();
 12+
 13+/** English
 14+ * @author Nad
 15+ */
 16+$messages['en'] = array(
 17+ 'security' => "Security log",
 18+ 'security-logpage' => "Security log",
 19+ 'security-logpagetext' => "This is a log of actions blocked by the [http://www.mediawiki.org/wiki/Extension:SimpleSecurity SimpleSecurity extension].",
 20+ 'security-logentry' => "",
 21+ 'badaccess-read' => "\nWarning: \"$1\" is referred to here, but you do not have sufficient permisions to access it.\n",
 22+ 'security-info' => "There are $1 on this article",
 23+ 'security-info-toggle' => "security restrictions",
 24+ 'security-inforestrict' => "$1 is restricted to $2",
 25+ 'security-desc-LS' => "<i>(applies because this article is in the <b>$2 $1</b>)</i>",
 26+ 'security-desc-PR' => "<i>(set from the <b>protect tab</b>)</i>",
 27+ 'security-desc-CR' => "<i>(this restriction is <b>in effect now</b>)</i>",
 28+ 'security-infosysops' => "No restrictions are in effect because you are a member of the <b>sysop</b> group",
 29+ 'security-manygroups' => "groups $1 and $2",
 30+ 'protect-unchain' => "Modify actions individually",
 31+);

Status & tagging log