PageRenderTime 47ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/extensions/SimpleSecurity/SimpleSecurity_body.php

https://github.com/ChuguluGames/mediawiki-svn
PHP | 466 lines | 288 code | 74 blank | 104 comment | 59 complexity | 73b7007e82c11467224dd65b65a2dd1d MD5 | raw file
  1. <?php
  2. /**
  3. * SimpleSecurity class
  4. */
  5. class SimpleSecurity {
  6. var $guid = '';
  7. var $cache = array();
  8. var $info = array(
  9. 'LS' => array(), # security info for rules from LocalSettings ($wgPageRestrictions)
  10. 'PR' => array(), # security info for rules from protect tab
  11. 'CR' => array() # security info for rules which are currently in effect
  12. );
  13. function __construct() {
  14. global $wgExtensionFunctions;
  15. # Put SimpleSecurity's setup function before all others
  16. array_unshift( $wgExtensionFunctions, array( $this, 'setup' ) );
  17. }
  18. function setup() {
  19. global $wgParser, $wgHooks, $wgLogTypes, $wgLogNames, $wgLogHeaders, $wgLogActions,
  20. $wgSecurityMagicIf, $wgSecurityMagicGroup, $wgSecurityExtraActions, $wgSecurityExtraGroups,
  21. $wgRestrictionTypes, $wgRestrictionLevels, $wgGroupPermissions,
  22. $wgSecurityRenderInfo, $wgSecurityAllowUnreadableLinks, $wgSecurityGroupsArticle;
  23. # Add our hooks
  24. $wgHooks['UserGetRights'][] = $this;
  25. $wgHooks['ImgAuthBeforeStream'][] = $this;
  26. if ( $wgSecurityMagicIf ) $wgParser->setFunctionHook( $wgSecurityMagicIf, array( $this, 'ifUserCan' ) );
  27. if ( $wgSecurityMagicGroup ) $wgParser->setFunctionHook( $wgSecurityMagicGroup, array( $this, 'ifGroup' ) );
  28. if ( $wgSecurityRenderInfo ) $wgHooks['OutputPageBeforeHTML'][] = $this;
  29. if ( $wgSecurityAllowUnreadableLinks ) $wgHooks['BeforePageDisplay'][] = $this;
  30. # Add a new log type
  31. $wgLogTypes[] = 'security';
  32. $wgLogNames ['security'] = 'securitylogpage';
  33. $wgLogHeaders['security'] = 'securitylogpagetext';
  34. $wgLogActions['security/deny'] = 'securitylogentry';
  35. # Each extra action is also a restriction type
  36. foreach ( $wgSecurityExtraActions as $k => $v ) {
  37. $wgRestrictionTypes[] = $k;
  38. }
  39. # Add extra available groups if $wgSecurityGroupsArticle is set
  40. if ( $wgSecurityGroupsArticle ) {
  41. $groups = new Article( Title::newFromText( $wgSecurityGroupsArticle, NS_MEDIAWIKI ) );
  42. if ( preg_match_all( "/^\*?\s*(.+?)\s*(\|\s*(.+))?$/m", $groups->getContent(), $match ) ) {
  43. foreach( $match[1] as $i => $k ) {
  44. $v = $match[3][$i];
  45. if ( $v ) $wgSecurityExtraGroups[strtolower( $k )] = $v;
  46. else $wgSecurityExtraGroups[strtolower( $k )] = '';
  47. }
  48. }
  49. }
  50. # Ensure the new groups show up in rights management
  51. # - note that 1.13 does a strange check in the ProtectionForm::buildSelector
  52. # $wgUser->isAllowed($key) where $key is an item from $wgRestrictionLevels
  53. # this requires that we treat the extra groups as an action and make sure its allowed by the user
  54. foreach ( $wgSecurityExtraGroups as $k => $v ) {
  55. if ( is_numeric( $k ) ) {
  56. $k = strtolower( $v );
  57. }
  58. $wgRestrictionLevels[] = $k;
  59. $wgGroupPermissions[$k][$k] = true; # members of $k must be allowed to perform $k
  60. $wgGroupPermissions['sysop'][$k] = true; # sysops must be allowed to perform $k as well
  61. }
  62. }
  63. /**
  64. * Process the ifUserCan conditional security directive
  65. */
  66. public function ifUserCan( &$parser, $action, $pagename, $then, $else = '' ) {
  67. return Title::newFromText( $pagename )->userCan( $action ) ? $then : $else;
  68. }
  69. /**
  70. * Process the ifGroup conditional security directive
  71. * - evaluates to true if current uset belongs to any of the comma-separated users and/or groups in the first parameter
  72. */
  73. public function ifGroup( &$parser, $groups, $then, $else = '' ) {
  74. global $wgUser;
  75. $intersection = array_intersect( array_map( 'strtolower', explode( ',', $groups ) ), $wgUser->getEffectiveGroups() );
  76. return count( $intersection ) > 0 ? $then : $else;
  77. }
  78. /**
  79. * Convert the urls with guids for hrefs into non-clickable text of class "unreadable"
  80. */
  81. public function onBeforePageDisplay( &$out ) {
  82. $out->mBodytext = preg_replace_callback(
  83. "|<a[^>]+title=\"(.+?)\".+?>(.+?)</a>|",
  84. array( $this, 'unreadableLink' ),
  85. $out->mBodytext
  86. );
  87. return true;
  88. }
  89. /**
  90. * Render security info if any restrictions on this title
  91. * Also make restricted pages not archive by robots
  92. */
  93. public function onOutputPageBeforeHTML( &$out, &$text ) {
  94. global $wgUser, $wgTitle;
  95. $title = $wgTitle;
  96. # Render info
  97. if ( is_object( $title ) && $title->exists() && count( $this->info['LS'] ) + count( $this->info['PR'] ) ) {
  98. $rights = $wgUser->getRights();
  99. $title->getRestrictions( false );
  100. $reqgroups = $title->mRestrictions;
  101. $sysop = in_array( 'sysop', $wgUser->getGroups() );
  102. # Build restrictions text
  103. $itext = "<ul>\n";
  104. foreach ( $this->info as $source => $rules ) if ( !( $sysop && $source === 'CR' ) ) {
  105. foreach ( $rules as $info ) {
  106. list( $action, $groups, $comment ) = $info;
  107. $gtext = $this->groupText( $groups );
  108. $itext .= "<li>" . wfMsg( 'security-inforestrict', "<b>$action</b>", $gtext ) . " $comment</li>\n";
  109. }
  110. }
  111. if ( $sysop ) $itext .= "<li>" . wfMsg( 'security-infosysops' ) . "</li>\n";
  112. $itext .= "</ul>\n";
  113. # Add some javascript to allow toggling the security-info
  114. $out->addScript( "<script type='text/javascript'>
  115. function toggleSecurityInfo() {
  116. var info = document.getElementById('security-info');
  117. info.style.display = info.style.display ? '' : 'none';
  118. }</script>"
  119. );
  120. # Add info-toggle before title and hidden info after title
  121. $link = "<a href='javascript:'>" . wfMsg( 'security-info-toggle' ) . "</a>";
  122. $link = "<span onClick='toggleSecurityInfo()'>$link</span>";
  123. $info = "<div id='security-info-toggle'>" . wfMsg( 'security-info', $link ) . "</div>\n";
  124. $text = "$info<div id='security-info' style='display:none'>$itext</div>\n$text";
  125. }
  126. return true;
  127. }
  128. /**
  129. * Callback function for unreadable link replacement
  130. */
  131. private function unreadableLink( $match ) {
  132. global $wgUser;
  133. return $this->userCanReadTitle( $wgUser, Title::newFromText( $match[1] ), $error )
  134. ? $match[0] : "<span class=\"unreadable\">$match[2]</span>";
  135. }
  136. /*
  137. * Check if image is accessible by current user when using img_auth
  138. */
  139. public function onImgAuthBeforeStream( &$title, &$path, &$name, &$result ) {
  140. global $wgUser;
  141. if ( !$this->userCanReadTitle( $wgUser, $title, $error )) {
  142. $result = array('img-auth-accessdenied', 'img-auth-noread', $name);
  143. return false;
  144. }
  145. return true;
  146. }
  147. /**
  148. * User::getRights returns a list of rights (allowed actions) based on the current users group membership
  149. * Title::getRestrictions returns a list of groups who can perform a particular action
  150. * So getRights should filter out any title-based restriction's actions which require groups that the user is not a member of
  151. * - Allows sysop access
  152. * - clears and populates the info array
  153. */
  154. public function onUserGetRights( $user, &$rights ) {
  155. global $wgGroupPermissions, $wgOut, $wgTitle, $wgRequest, $wgPageRestrictions;
  156. # Hack to prevent specialpage operations on unreadable pages
  157. if ( !is_object( $wgTitle ) ) return true;
  158. $title = $wgTitle;
  159. $ns = $title->getNamespace();
  160. if ( $ns == NS_SPECIAL ) {
  161. list( $name, $par ) = explode( '/', $title->getDBkey() . '/', 2 );
  162. if ( $par ) $title = Title::newFromText( $par );
  163. elseif ( $wgRequest->getVal( 'target' ) ) $title = Title::newFromText( $wgRequest->getVal( 'target' ) );
  164. elseif ( $wgRequest->getVal( 'oldtitle' ) ) $title = Title::newFromText( $wgRequest->getVal( 'oldtitle' ) );
  165. }
  166. if ( !is_object( $title ) ) return true; # If still no usable title bail
  167. $groups = $user->getEffectiveGroups();
  168. # Filter rights according to $wgPageRestrictions
  169. # - also update LS (rules from local settings) items to info array
  170. $this->pageRestrictions( $rights, $groups, $title, true );
  171. # Add PR (rules from article's protect tab) items to info array
  172. # - allows rules in protection tab to override those from $wgPageRestrictions
  173. if ( !$title->mRestrictionsLoaded ) $title->loadRestrictions();
  174. foreach ( $title->mRestrictions as $a => $g ) if ( count( $g ) ) {
  175. $this->info['PR'][] = array( $a, $g, wfMsg( 'security-desc-PR' ) );
  176. if ( array_intersect( $groups, $g ) ) $rights[] = $a;
  177. }
  178. # If title is not readable by user, remove the read and move rights, and tell robots not to archive
  179. if ( !in_array( 'sysop', $groups ) && !$this->userCanReadTitle( $user, $title, $error ) ) {
  180. foreach ( $rights as $i => $right ) if ( $right === 'read' || $right === 'move' ) unset( $rights[$i] );
  181. # $this->info['CR'] = array('read', '', '');
  182. $wgOut->addMeta( 'robots', 'noarchive' );
  183. }
  184. return true;
  185. }
  186. /**
  187. * Patches SQL queries to ensure that the old_id field is present in all requests for the old_text field
  188. * otherwise the title that the old_text is associated with can't be determined
  189. */
  190. static function patchSQL( $sql ) {
  191. return preg_replace_callback( "/^SELECT\b\s*(.+?)\s*\bFROM\b/i", 'SimpleSecurity::patchSQL_internal', $sql, 1 );
  192. }
  193. /**
  194. * Callback for patchSQL()
  195. */
  196. static private function patchSQL_internal( $match ) {
  197. if ( !preg_match( "/old_text/", $match[1] ) ) return $match[0];
  198. $fields = str_replace( " ", "", $match[1] );
  199. return ( preg_match( "/old_id/", $fields ) ) ? $match[0] : "SELECT $fields, old_id FROM";
  200. }
  201. /**
  202. * Validate the passed database row and replace any invalid content
  203. * - called from fetchObject hook whenever a row contains old_text
  204. * - old_id is guaranteed to exist due to patchSQL method
  205. * - bails if sysop
  206. */
  207. public function validateRow( &$row ) {
  208. global $wgUser;
  209. $groups = $wgUser->getEffectiveGroups();
  210. if ( in_array( 'sysop', $groups ) || empty( $row->old_id ) ) return;
  211. # Obtain a title object from the old_id
  212. $dbr = wfGetDB( DB_SLAVE );
  213. $tbl = $dbr->tableName( 'revision' );
  214. $rev = $dbr->selectRow( $tbl, 'rev_page', "rev_text_id = {$row->old_id}", __METHOD__ );
  215. $title = Title::newFromID( $rev->rev_page );
  216. # Replace text content in the passed database row if title unreadable by user
  217. if ( !$this->userCanReadTitle( $wgUser, $title, $error ) ) $row->old_text = $error;
  218. }
  219. /**
  220. * Return bool for whether or not passed user has read access to the passed title
  221. * - if there are read restrictions in place for the title, check if user a member of any groups required for read access
  222. */
  223. public function userCanReadTitle( &$user, &$title, &$error ) {
  224. $groups = $user->getEffectiveGroups();
  225. if ( !is_object( $title ) || in_array( 'sysop', $groups ) ) return true;
  226. # Retrieve result from cache if exists (for re-use within current request)
  227. $key = $user->getID() . '\x07' . $title->getPrefixedText();
  228. if ( array_key_exists( $key, $this->cache ) ) {
  229. $error = $this->cache[$key][1];
  230. return $this->cache[$key][0];
  231. }
  232. # Determine readability based on $wgPageRestrictions
  233. $rights = array( 'read' );
  234. $this->pageRestrictions( $rights, $groups, $title );
  235. $readable = count( $rights ) > 0;
  236. # If there are title restrictions that prevent reading, they override $wgPageRestrictions readability
  237. $whitelist = $title->getRestrictions( 'read' );
  238. if ( count( $whitelist ) > 0 && !count( array_intersect( $whitelist, $groups ) ) > 0 ) $readable = false;
  239. $error = $readable ? "" : wfMsg( 'badaccess-read', $title->getPrefixedText() );
  240. $this->cache[$key] = array( $readable, $error );
  241. return $readable;
  242. }
  243. /**
  244. * Returns a textual description of the passed list
  245. */
  246. private function groupText( &$groups ) {
  247. $gl = $groups;
  248. $gt = array_pop( $gl );
  249. // FIXME: use $wgLang->commafy()
  250. // FIXME: hard coded bold. Not all scripts use this. Needs i18n support.
  251. if ( count( $groups ) > 1 ) $gt = wfMsg( 'security-manygroups', "<b>" . join( "</b>, <b>", $gl ) . "</b>", "<b>$gt</b>" );
  252. else $gt = "the <b>$gt</b> group"; // FIXME: hard coded text. Needs i18n support.
  253. return $gt;
  254. }
  255. /**
  256. * Reduce the passed list of rights based on $wgPageRestrictions and the passed groups and title
  257. * $wgPageRestrictions contains category and namespace based permissions rules
  258. * the format of the rules is [type][action] = group(s)
  259. * also adds LS items and currently active LS to info array
  260. */
  261. private function pageRestrictions( &$rights, &$groups, &$title, $updateInfo = false ) {
  262. global $wgPageRestrictions;
  263. $cats = array();
  264. foreach ( $wgPageRestrictions as $k => $restriction ) if ( preg_match( '/^(.+?):(.*)$/', $k, $m ) ) {
  265. $type = ucfirst( $m[1] );
  266. $data = $m[2];
  267. $deny = false;
  268. # Validate rule against the title based on its type
  269. switch ( $type ) {
  270. case "Category":
  271. # If processing first category rule, build a list of cats this article belongs to
  272. if ( count( $cats ) == 0 ) {
  273. $dbr = wfGetDB( DB_SLAVE );
  274. $cl = $dbr->tableName( 'categorylinks' );
  275. $id = $title->getArticleID();
  276. $res = $dbr->select( $cl, 'cl_to', "cl_from = '$id'", __METHOD__, array( 'ORDER BY' => 'cl_sortkey' ) );
  277. while ( $row = $dbr->fetchRow( $res ) ) $cats[] = $row[0];
  278. $dbr->freeResult( $res );
  279. }
  280. $deny = in_array( $data, $cats );
  281. break;
  282. case "Namespace":
  283. $deny = $data == $title->getNsText();
  284. break;
  285. }
  286. # If the rule applies to this title, check if we're a member of the required groups,
  287. # remove action from rights list if not (can be mulitple occurences)
  288. # - also update info array with page-restriction that apply to this title (LS), and rules in effect for this user (CR)
  289. if ( $deny ) {
  290. foreach ( $restriction as $action => $reqgroups ) {
  291. if ( !is_array( $reqgroups ) ) {
  292. $reqgroups = array( $reqgroups );
  293. }
  294. if ( $updateInfo ) {
  295. $this->info['LS'][] = array( $action, $reqgroups, wfMsg( 'security-desc-LS', wfMsg( 'security-type-' . strtolower( $type ) ), $data ) );
  296. }
  297. if ( !in_array( 'sysop', $groups ) && !array_intersect( $groups, $reqgroups ) ) {
  298. foreach ( $rights as $i => $right ) if ( $right === $action ) unset( $rights[$i] );
  299. # $this->info['CR'][] = array($action, $reqgroups, wfMsg('security-desc-CR'));
  300. }
  301. }
  302. }
  303. }
  304. }
  305. /**
  306. * Create the new Database class with hooks in its query() and fetchObject() methods and use our LBFactory_SimpleSecurity class
  307. */
  308. static function applyDatabaseHook() {
  309. global $wgDBtype, $wgLBFactoryConf;
  310. # Create a new "Database_SimpleSecurity" database class with hooks into its query() and fetchObject() methods
  311. # - hooks are added in a sub-class of the database type specified in $wgDBtype
  312. # - query method is overriden to ensure that old_id field is returned for all queries which read old_text field
  313. # - only SELECT statements are ever patched
  314. # - fetchObject method is overridden to validate row content based on old_id
  315. eval( 'class Database_SimpleSecurity extends Database' . ucfirst( $wgDBtype ) . ' {
  316. public function query( $sql, $fname = "", $tempIgnore = false ) {
  317. return parent::query( SimpleSecurity::PatchSQL( $sql ), $fname, $tempIgnore );
  318. }
  319. function fetchObject( $res ) {
  320. global $wgSimpleSecurity;
  321. $row = parent::fetchObject( $res );
  322. if( isset( $row->old_text ) ) $wgSimpleSecurity->validateRow( $row );
  323. return $row;
  324. }
  325. }' );
  326. # Make sure our new LBFactory is used which in turn uses our LoadBalancer and Database classes
  327. $wgLBFactoryConf = array( 'class' => 'LBFactory_SimpleSecurity' );
  328. }
  329. }
  330. /**
  331. * The new LBFactory_SimpleSecurity class identical to LBFactory_Simple except that it returns a LoadBalancer_SimpleSecurity object
  332. */
  333. class LBFactory_SimpleSecurity extends LBFactory_Simple {
  334. function newMainLB( $wiki = false ) {
  335. global $wgDBservers, $wgMasterWaitTimeout;
  336. if ( $wgDBservers ) {
  337. $servers = $wgDBservers;
  338. } else {
  339. global $wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, $wgDBtype, $wgDebugDumpSql;
  340. $servers = array(array(
  341. 'host' => $wgDBserver,
  342. 'user' => $wgDBuser,
  343. 'password' => $wgDBpassword,
  344. 'dbname' => $wgDBname,
  345. 'type' => $wgDBtype,
  346. 'load' => 1,
  347. 'flags' => ($wgDebugDumpSql ? DBO_DEBUG : 0) | DBO_DEFAULT
  348. ));
  349. }
  350. return new LoadBalancer_SimpleSecurity( array(
  351. 'servers' => $servers,
  352. 'masterWaitTimeout' => $wgMasterWaitTimeout
  353. ));
  354. }
  355. }
  356. /**
  357. * LoadBalancer_SimpleSecurity always returns Database_SimpleSecurity regardles of $wgDBtype
  358. */
  359. class LoadBalancer_SimpleSecurity extends LoadBalancer {
  360. function reallyOpenConnection( $server, $dbNameOverride = false ) {
  361. if( !is_array( $server ) ) {
  362. throw new MWException( 'You must update your load-balancing configuration. See DefaultSettings.php entry for $wgDBservers.' );
  363. }
  364. $host = $server['host'];
  365. $dbname = $server['dbname'];
  366. if ( $dbNameOverride !== false ) {
  367. $server['dbname'] = $dbname = $dbNameOverride;
  368. }
  369. wfDebug( "Connecting to $host $dbname...\n" );
  370. $db = new Database_SimpleSecurity(
  371. isset( $server['host'] ) ? $server['host'] : false,
  372. isset( $server['user'] ) ? $server['user'] : false,
  373. isset( $server['password'] ) ? $server['password'] : false,
  374. isset( $server['dbname'] ) ? $server['dbname'] : false,
  375. isset( $server['flags'] ) ? $server['flags'] : 0,
  376. isset( $server['tableprefix'] ) ? $server['tableprefix'] : 'get from global'
  377. );
  378. if ( $db->isOpen() ) {
  379. wfDebug( "Connected to $host $dbname.\n" );
  380. } else {
  381. wfDebug( "Connection failed to $host $dbname.\n" );
  382. }
  383. $db->setLBInfo( $server );
  384. if ( isset( $server['fakeSlaveLag'] ) ) {
  385. $db->setFakeSlaveLag( $server['fakeSlaveLag'] );
  386. }
  387. if ( isset( $server['fakeMaster'] ) ) {
  388. $db->setFakeMaster( true );
  389. }
  390. return $db;
  391. }
  392. }