r48858 MediaWiki - Code Review archive

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

Diff [purge]

Index: trunk/extensions/SimpleSecurity/SimpleSecurity.php
@@ -4,12 +4,13 @@
55 * - Extends the MediaWiki article protection to allow restricting viewing of article content
66 * - Also adds #ifusercan and #ifgroup parser functions for rendering restriction-based content
77 *
8 - * See http://www.mediawiki.org/Extension:Simple_Security for installation and usage details
 8+ * See http://www.mediawiki.org/Extension:SimpleSecurity for installation and usage details
99 * See http://www.organicdesign.co.nz/Extension_talk:SimpleSecurity.php for development notes and disucssion
1010 *
1111 * Version 4.0 started Oct 2007 - new version for modern MediaWiki's using DatabaseFetchHook
1212 * Version 4.1 started Jun 2008 - development funded for a slimmed down functional version
1313 * Version 4.2 started Aug 2008 - fattened up a bit again - $wgPageRestrictions and security info added in again
 14+ * Version 4.3 started Mar 2009 - bug fixes and split out to separate class and i18n files
1415 *
1516 * @package MediaWiki
1617 * @subpackage Extensions
@@ -21,8 +22,13 @@
2223 if ( !defined( 'MEDIAWIKI' ) ) die( 'Not an entry point.' );
2324 if ( version_compare( $wgVersion, '1.12.0' ) < 0 ) die( 'Sorry, this extension requires at least MediaWiki version 1.12.0' );
2425
25 -define( 'SIMPLESECURITY_VERSION', '4.3.0, 2009-03-24' );
 26+define( 'SIMPLESECURITY_VERSION', '4.3.1, 2009-03-26' );
2627
 28+# Load the SimpleSecurity class and messages
 29+$dir = dirname( __FILE__ ) . '/';
 30+$wgExtensionMessagesFiles['SimpleSecurity'] = $dir . 'SimpleSecurity.i18n.php';
 31+$wgAutoloadClasses['SimpleSecurity'] = $dir . 'SimpleSecurity_body.php';
 32+
2733 # Global security settings
2834 $wgSecurityMagicIf = "ifusercan"; # the name for doing a permission-based conditional
2935 $wgSecurityMagicGroup = "ifgroup"; # the name for doing a group-based conditional
@@ -61,344 +67,6 @@
6268 if ( !isset( $wgSecurityUseDBHook ) ) $wgSecurityUseDBHook = false;
6369 if ( $wgSecurityUseDBHook && class_exists( 'Database' ) ) wfSimpleSecurityDBHook();
6470
65 -class SimpleSecurity {
66 -
67 - var $guid = '';
68 - var $cache = array();
69 - var $info = array();
70 -
71 - function __construct() {
72 - global $wgParser, $wgHooks, $wgLogTypes, $wgLogNames, $wgLogHeaders, $wgLogActions, $wgMessageCache,
73 - $wgSecurityMagicIf, $wgSecurityMagicGroup, $wgSecurityExtraActions, $wgSecurityExtraGroups,
74 - $wgRestrictionTypes, $wgRestrictionLevels, $wgGroupPermissions,
75 - $wgSecurityRenderInfo, $wgSecurityAllowUnreadableLinks;
76 -
77 - # $wgGroupPermissions has to have its default read entry removed because Title::userCanRead checks it directly
78 - if ( $this->default_read = ( isset( $wgGroupPermissions['*']['read'] ) && $wgGroupPermissions['*']['read'] ) )
79 - $wgGroupPermissions['*']['read'] = false;
80 -
81 - # Add our hooks
82 - $wgHooks['UserGetRights'][] = $this;
83 - if ( $wgSecurityMagicIf ) $wgParser->setFunctionHook( $wgSecurityMagicIf, array( $this, 'ifUserCan' ) );
84 - if ( $wgSecurityMagicGroup ) $wgParser->setFunctionHook( $wgSecurityMagicGroup, array( $this, 'ifGroup' ) );
85 - if ( $wgSecurityAllowUnreadableLinks ) $wgHooks['BeforePageDisplay'][] = $this;
86 - if ( $wgSecurityRenderInfo ) $wgHooks['OutputPageBeforeHTML'][] = $this;
87 -
88 - # Add a new log type
89 - $wgLogTypes[] = 'security';
90 - $wgLogNames ['security'] = 'securitylogpage';
91 - $wgLogHeaders['security'] = 'securitylogpagetext';
92 - $wgLogActions['security/deny'] = 'securitylogentry';
93 -
94 - # Extend protection form groups, actions and messages
95 - $wgMessageCache->addMessages( array( 'protect-unchain' => "Modify actions individually" ) );
96 - $wgMessageCache->addMessages( array( 'badaccess-group1' => wfMsg( 'badaccess-group0' ) ) );
97 - $wgMessageCache->addMessages( array( 'badaccess-group2' => wfMsg( 'badaccess-group0' ) ) );
98 - $wgMessageCache->addMessages( array( 'badaccess-groups' => wfMsg( 'badaccess-group0' ) ) );
99 -
100 - foreach ( $wgSecurityExtraActions as $k => $v ) {
101 - if ( empty( $v ) ) $v = ucfirst( $k );
102 - $wgRestrictionTypes[] = $k;
103 - $wgMessageCache->addMessages( array( "restriction-$k" => $v ) );
104 - }
105 -
106 - # Ensure the new groups show up in rights management
107 - # - note that 1.13 does a strange check in the ProtectionForm::buildSelector
108 - # $wgUser->isAllowed($key) where $key is an item from $wgRestrictionLevels
109 - # this requires that we treat the extra groups as an action and make sure its allowed by the user
110 - foreach ( $wgSecurityExtraGroups as $k => $v ) {
111 - if ( empty( $v ) ) $v = ucfirst( $k );
112 - $wgRestrictionLevels[] = $k;
113 - $wgMessageCache->addMessages( array( "protect-level-$k" => $v ) );
114 - $wgGroupPermissions[$k][$k] = true; # members of $k must be allowed to perform $k
115 - $wgGroupPermissions['sysop'][$k] = true; # sysops must be allowed to perform $k as well
116 - }
117 - }
118 -
119 - /**
120 - * Process the ifUserCan conditional security directive
121 - */
122 - public function ifUserCan( &$parser, $action, $pagename, $then, $else = '' ) {
123 - return Title::newFromText( $pagename )->userCan( $action ) ? $then : $else;
124 - }
125 -
126 - /**
127 - * Process the ifGroup conditional security directive
128 - * - evaluates to true if current uset belongs to any of the comma-separated users and/or groups in the first parameter
129 - */
130 - public function ifGroup( &$parser, $groups, $then, $else = '' ) {
131 - global $wgUser;
132 - $intersection = array_intersect( array_map( 'strtolower', split( ',', $groups ) ), $wgUser->getEffectiveGroups() );
133 - return count( $intersection ) > 0 ? $then : $else;
134 - }
135 -
136 - /**
137 - * Convert the urls with guids for hrefs into non-clickable text of class "unreadable"
138 - */
139 - public function onBeforePageDisplay( &$out ) {
140 - $out->mBodytext = preg_replace_callback(
141 - "|<a[^>]+title=\"(.+?)\".+?>(.+?)</a>|",
142 - array( $this, 'unreadableLink' ),
143 - $out->mBodytext
144 - );
145 - return true;
146 - }
147 -
148 - /**
149 - * Render security info if any restrictions on this title
150 - */
151 - public function onOutputPageBeforeHTML( &$out, &$text ) {
152 - global $wgTitle, $wgUser;
153 -
154 - # Render security info if any
155 - if ( is_object( $wgTitle ) && $wgTitle->exists() && count( $this->info['LS'] ) + count( $this->info['PR'] ) ) {
156 -
157 - $rights = $wgUser->getRights();
158 - $wgTitle->getRestrictions( false );
159 - $reqgroups = $wgTitle->mRestrictions;
160 - $sysop = in_array( 'sysop', $wgUser->getGroups() );
161 -
162 - # Build restrictions text
163 - $itext = "<ul>\n";
164 - foreach ( $this->info as $source => $rules ) if ( !( $sysop && $source === 'CR' ) ) {
165 - foreach ( $rules as $info ) {
166 - list( $action, $groups, $comment ) = $info;
167 - $gtext = $this->groupText( $groups );
168 - $itext .= "<li>" . wfMsg( 'security-inforestrict', "<b>$action</b>", $gtext ) . " $comment</li>\n";
169 - }
170 - }
171 - if ( $sysop ) $itext .= "<li>" . wfMsg( 'security-infosysops' ) . "</li>\n";
172 - $itext .= "</ul>\n";
173 -
174 - # Add some javascript to allow toggling the security-info
175 - $out->addScript( "<script type='text/javascript'>
176 - function toggleSecurityInfo() {
177 - var info = document.getElementById('security-info');
178 - info.style.display = info.style.display ? '' : 'none';
179 - }</script>"
180 - );
181 -
182 - # Add info-toggle before title and hidden info after title
183 - $link = "<a href='javascript:'>" . wfMsg( 'security-info-toggle' ) . "</a>";
184 - $link = "<span onClick='toggleSecurityInfo()'>$link</span>";
185 - $info = "<div id='security-info-toggle'>" . wfMsg( 'security-info', $link ) . "</div>\n";
186 - $text = "$info<div id='security-info' style='display:none'>$itext</div>\n$text";
187 - }
188 -
189 - return true;
190 - }
191 -
192 - /**
193 - * Callback function for unreadable link replacement
194 - */
195 - private function unreadableLink( $match ) {
196 - global $wgUser;
197 - return $this->userCanReadTitle( $wgUser, Title::newFromText( $match[1] ), $error )
198 - ? $match[0] : "<span class=\"unreadable\">$match[2]</span>";
199 - }
200 -
201 - /**
202 - * User::getRights returns a list of rights (allowed actions) based on the current users group membership
203 - * Title::getRestrictions returns a list of groups who can perform a particular action
204 - * So getRights should filter out any title-based restriction's actions which require groups that the user is not a member of
205 - * - Allows sysop access
206 - * - clears and populates the info array
207 - */
208 - public function onUserGetRights( &$user, &$rights ) {
209 - global $wgGroupPermissions, $wgTitle, $wgRequest, $wgPageRestrictions;
210 -
211 - # Hack to prevent specialpage operations on unreadable pages
212 - if ( !is_object( $wgTitle ) ) return true;
213 - $title = $wgTitle;
214 - $ns = $title->getNamespace();
215 - if ( $ns == NS_SPECIAL ) {
216 - list( $name, $par ) = explode( '/', $title->getDBkey() . '/', 2 );
217 - if ( $par ) $title = Title::newFromText( $par);
218 - elseif ( $wgRequest->getVal( 'target' ) ) $title = Title::newFromText( $wgRequest->getVal( 'target' ) );
219 - elseif ( $wgRequest->getVal( 'oldtitle' ) ) $title = Title::newFromText( $wgRequest->getVal( 'oldtitle' ) );
220 - }
221 - if ( !is_object( $title ) ) return true; # If still no usable title bail
222 -
223 - $this->info['LS'] = array(); # security info for rules from LocalSettings ($wgPageRestrictions)
224 - $this->info['PR'] = array(); # security info for rules from protect tab
225 - $this->info['CR'] = array(); # security info for rules which are currently in effect
226 - $groups = $user->getEffectiveGroups();
227 -
228 - # Put the anon read right back in $wgGroupPermissions if it was there initially
229 - # - it had to be removed because Title::userCanRead short-circuits with it
230 - if ( $this->default_read ) {
231 - $wgGroupPermissions['*']['read'] = true;
232 - $rights[] = 'read';
233 - }
234 -
235 - # Filter rights according to $wgPageRestrictions
236 - # - also update LS (rules from local settings) items to info array
237 - $this->pageRestrictions( $rights, $groups, $title, true );
238 -
239 - # Add PR (rules from article's protect tab) items to info array
240 - # - allows rules in protection tab to override those from $wgPageRestrictions
241 - if ( !$title->mRestrictionsLoaded ) $title->loadRestrictions();
242 - foreach ( $title->mRestrictions as $a => $g ) if ( count( $g ) ) {
243 - $this->info['PR'][] = array( $a, $g, wfMsg( 'security-desc-PR' ) );
244 - if ( array_intersect( $groups, $g ) ) $rights[] = $a;
245 - }
246 -
247 - # If title is not readable by user, remove the read and move rights
248 - if ( !in_array( 'sysop', $groups ) && !$this->userCanReadTitle( $user, $title, $error ) ) {
249 - foreach ( $rights as $i => $right ) if ( $right === 'read' || $right === 'move' ) unset( $rights[$i] );
250 - #$this->info['CR'] = array('read', '', '');
251 - }
252 -
253 - return true;
254 - }
255 -
256 - /**
257 - * Patches SQL queries to ensure that the old_id field is present in all requests for the old_text field
258 - * otherwise the title that the old_text is associated with can't be determined
259 - */
260 - static function patchSQL( $match ) {
261 - if ( !preg_match( "/old_text/", $match[0] ) ) return $match[0];
262 - $fields = str_replace( " ", "", $match[0] );
263 - return ( $fields == "*" || preg_match( "/old_id/", $fields ) ) ? $fields : "$fields,old_id";
264 - }
265 -
266 - /**
267 - * Validate the passed database row and replace any invalid content
268 - * - called from fetchObject hook whenever a row contains old_text
269 - * - old_id is guaranteed to exist due to patchSQL method
270 - * - bails if sysop
271 - */
272 - public function validateRow( &$row ) {
273 - global $wgUser;
274 - $groups = $wgUser->getEffectiveGroups();
275 - if ( in_array( 'sysop', $groups ) || empty( $row->old_id ) ) return;
276 -
277 - # Obtain a title object from the old_id
278 - $dbr = wfGetDB( DB_SLAVE );
279 - $tbl = $dbr->tableName( 'revision' );
280 - $rev = $dbr->selectRow( $tbl, 'rev_page', "rev_text_id = {$row->old_id}", __METHOD__ );
281 - $title = Title::newFromID( $rev->rev_page );
282 -
283 - # Replace text content in the passed database row if title unreadable by user
284 - if ( !$this->userCanReadTitle( $wgUser, $title, $error ) ) $row->old_text = $error;
285 - }
286 -
287 - /**
288 - * Return bool for whether or not passed user has read access to the passed title
289 - * - if there are read restrictions in place for the title, check if user a member of any groups required for read access
290 - */
291 - public function userCanReadTitle( &$user, &$title, &$error ) {
292 - $groups = $user->getEffectiveGroups();
293 - if ( !is_object( $title ) || in_array( 'sysop', $groups ) ) return true;
294 -
295 - # Retrieve result from cache if exists (for re-use within current request)
296 - $key = $user->getID().'\x07'.$title->getPrefixedText();
297 - if (array_key_exists($key, $this->cache)) {
298 - $error = $this->cache[$key][1];
299 - return $this->cache[$key][0];
300 - }
301 -
302 - # Determine readability based on $wgPageRestrictions
303 - $rights = array( 'read' );
304 - $this->pageRestrictions( $rights, $groups, $title );
305 - $readable = count( $rights ) > 0;
306 -
307 - # If there are title restrictions that prevent reading, they override $wgPageRestrictions readability
308 - $whitelist = $title->getRestrictions( 'read' );
309 - if ( count( $whitelist ) > 0 && !count( array_intersect( $whitelist, $groups ) ) > 0 ) $readable = false;
310 -
311 - $error = $readable ? "" : wfMsg( 'badaccess-read', $title->getPrefixedText() );
312 - $this->cache[$key] = array( $readable, $error );
313 - return $readable;
314 - }
315 -
316 - /**
317 - * Returns a textual description of the passed list
318 - */
319 - private function groupText( &$groups ) {
320 - $gl = $groups;
321 - $gt = array_pop( $gl );
322 - if ( count( $groups ) > 1 ) $gt = wfMsg( 'security-manygroups', "<b>" . join( "</b>, <b>", $gl ) . "</b>", "<b>$gt</b>" );
323 - else $gt = "the <b>$gt</b> group";
324 - return $gt;
325 - }
326 -
327 - /**
328 - * Reduce the passed list of rights based on $wgPageRestrictions and the passed groups and title
329 - * $wgPageRestrictions contains category and namespace based permissions rules
330 - * the format of the rules is [type][action] = group(s)
331 - * also adds LS items and currently active LS to info array
332 - */
333 - private function pageRestrictions( &$rights, &$groups, &$title, $updateInfo = false ) {
334 - global $wgPageRestrictions;
335 - $cats = array();
336 - foreach ( $wgPageRestrictions as $k => $restriction ) if ( preg_match( '/^(.+?):(.*)$/', $k, $m ) ) {
337 - $type = ucfirst( $m[1] );
338 - $data = $m[2];
339 - $deny = false;
340 -
341 - # Validate rule against the title based on its type
342 - switch ($type) {
343 -
344 - case "Category":
345 -
346 - # If processing first category rule, build a list of cats this article belongs to
347 - if ( count( $cats ) == 0 ) {
348 - $dbr = wfGetDB( DB_SLAVE );
349 - $cl = $dbr->tableName( 'categorylinks' );
350 - $id = $title->getArticleID();
351 - $res = $dbr->select( $cl, 'cl_to', "cl_from = '$id'", __METHOD__, array( 'ORDER BY' => 'cl_sortkey' ) );
352 - while ( $row = $dbr->fetchRow( $res ) ) $cats[] = $row[0];
353 - $dbr->freeResult( $res );
354 - }
355 -
356 - $deny = in_array( $data, $cats );
357 - break;
358 -
359 - case "Namespace":
360 - $deny = $data == $title->getNsText();
361 - break;
362 - }
363 -
364 - # If the rule applies to this title, check if we're a member of the required groups,
365 - # remove action from rights list if not (can be mulitple occurences)
366 - # - also update info array with page-restriction that apply to this title (LS), and rules in effect for this user (CR)
367 - if ( $deny ) {
368 - foreach ( $restriction as $action => $reqgroups ) {
369 - if ( !is_array( $reqgroups ) ) $reqgroups = array( $reqgroups );
370 - if ( $updateInfo ) $this->info['LS'][] = array( $action, $reqgroups, wfMsg( 'security-desc-LS', strtolower( $type ), $data ) );
371 - if ( !in_array( 'sysop', $groups ) && !array_intersect( $groups, $reqgroups ) ) {
372 - foreach ( $rights as $i => $right ) if ( $right === $action ) unset( $rights[$i] );
373 - #$this->info['CR'][] = array($action, $reqgroups, wfMsg('security-desc-CR'));
374 - }
375 - }
376 - }
377 - }
378 - }
379 -
380 - /**
381 - * Updates passed LoadBalancer's DB servers to secure class
382 - */
383 - static function updateLB( &$lb ) {
384 - $lb->closeAll();
385 - foreach ( $lb->mServers as $i => $server ) $lb->mServers[$i]['type'] = 'SimpleSecurity';
386 - }
387 -
388 - /**
389 - * Hack to ensure proper search class is used
390 - * - $wgDBtype determines search class unless already defined in $wgSearchType
391 - * - just copied method from SearchEngine::create()
392 - */
393 - static function fixSearchType() {
394 - global $wgDBtype, $wgSearchType;
395 - if ( $wgSearchType ) return;
396 - elseif ( $wgDBtype == 'mysql' ) $wgSearchType = 'SearchMySQL4';
397 - elseif ( $wgDBtype == 'postgres' ) $wgSearchType = 'SearchPostgres';
398 - elseif ( $wgDBtype == 'oracle' ) $wgSearchType = 'SearchOracle';
399 - else $wgSearchType = 'SearchEngineDummy';
400 - }
401 -}
402 -
40371 /**
40472 * Hook into Database::query and Database::fetchObject of database instances
40573 * - this can't be executed from within a method because PHP doesn't like nested class definitions
@@ -466,24 +134,5 @@
467135 # such as $wgContLang->stripForSearch which is called by SearchMySQL::parseQuery
468136 wfGetDB( DB_MASTER );
469137 $wgDBtype = $wgOldDBtype;
470 -
471 - # Add messages
472 - if ( $wgLanguageCode == 'en' ) {
473 - $wgMessageCache->addMessages( array(
474 - 'security' => "Security log",
475 - 'security-logpage' => "Security log",
476 - 'security-logpagetext' => "This is a log of actions blocked by the [[MW:Extension:SimpleSecurity|SimpleSecurity extension]].",
477 - 'security-logentry' => "",
478 - 'badaccess-read' => "\nWarning: \"$1\" is referred to here, but you do not have sufficient permisions to access it.\n",
479 - 'security-info' => "There are $1 on this article",
480 - 'security-info-toggle' => "security restrictions",
481 - 'security-inforestrict' => "$1 is restricted to $2",
482 - 'security-desc-LS' => "<i>(applies because this article is in the <b>$2 $1</b>)</i>",
483 - 'security-desc-PR' => "<i>(set from the <b>protect tab</b>)</i>",
484 - 'security-desc-CR' => "<i>(this restriction is <b>in effect now</b>)</i>",
485 - 'security-infosysops' => "No restrictions are in effect because you are a member of the <b>sysop</b> group",
486 - 'security-manygroups' => "groups $1 and $2"
487 - ) );
488 - }
489138 }
490139

Status & tagging log