Index: trunk/extensions/SimpleSecurity/SimpleSecurity.php |
— | — | @@ -4,12 +4,13 @@ |
5 | 5 | * - Extends the MediaWiki article protection to allow restricting viewing of article content |
6 | 6 | * - Also adds #ifusercan and #ifgroup parser functions for rendering restriction-based content |
7 | 7 | * |
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 |
9 | 9 | * See http://www.organicdesign.co.nz/Extension_talk:SimpleSecurity.php for development notes and disucssion |
10 | 10 | * |
11 | 11 | * Version 4.0 started Oct 2007 - new version for modern MediaWiki's using DatabaseFetchHook |
12 | 12 | * Version 4.1 started Jun 2008 - development funded for a slimmed down functional version |
13 | 13 | * 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 |
14 | 15 | * |
15 | 16 | * @package MediaWiki |
16 | 17 | * @subpackage Extensions |
— | — | @@ -21,8 +22,13 @@ |
22 | 23 | if ( !defined( 'MEDIAWIKI' ) ) die( 'Not an entry point.' ); |
23 | 24 | if ( version_compare( $wgVersion, '1.12.0' ) < 0 ) die( 'Sorry, this extension requires at least MediaWiki version 1.12.0' ); |
24 | 25 | |
25 | | -define( 'SIMPLESECURITY_VERSION', '4.3.0, 2009-03-24' ); |
| 26 | +define( 'SIMPLESECURITY_VERSION', '4.3.1, 2009-03-26' ); |
26 | 27 | |
| 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 | + |
27 | 33 | # Global security settings |
28 | 34 | $wgSecurityMagicIf = "ifusercan"; # the name for doing a permission-based conditional |
29 | 35 | $wgSecurityMagicGroup = "ifgroup"; # the name for doing a group-based conditional |
— | — | @@ -61,344 +67,6 @@ |
62 | 68 | if ( !isset( $wgSecurityUseDBHook ) ) $wgSecurityUseDBHook = false; |
63 | 69 | if ( $wgSecurityUseDBHook && class_exists( 'Database' ) ) wfSimpleSecurityDBHook(); |
64 | 70 | |
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 | | - |
403 | 71 | /** |
404 | 72 | * Hook into Database::query and Database::fetchObject of database instances |
405 | 73 | * - this can't be executed from within a method because PHP doesn't like nested class definitions |
— | — | @@ -466,24 +134,5 @@ |
467 | 135 | # such as $wgContLang->stripForSearch which is called by SearchMySQL::parseQuery |
468 | 136 | wfGetDB( DB_MASTER ); |
469 | 137 | $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 | | - } |
489 | 138 | } |
490 | 139 | |