PageRenderTime 25ms CodeModel.GetById 11ms RepoModel.GetById 1ms app.codeStats 0ms

/extensions/TitleBlacklist/TitleBlacklist.list.php

https://github.com/ChuguluGames/mediawiki-svn
PHP | 478 lines | 286 code | 35 blank | 157 comment | 77 complexity | 74031b80e41d63414fdf11d4d616ab57 MD5 | raw file
  1. <?php
  2. /**
  3. * Title Blacklist class
  4. * @author Victor Vasiliev
  5. * @copyright © 2007-2010 Victor Vasiliev et al
  6. * @license GNU General Public License 2.0 or later
  7. * @file
  8. */
  9. //@{
  10. /**
  11. * @ingroup Extensions
  12. */
  13. /**
  14. * Implements a title blacklist for MediaWiki
  15. */
  16. class TitleBlacklist {
  17. private $mBlacklist = null, $mWhitelist = null;
  18. const VERSION = 2; //Blacklist format
  19. /**
  20. * Get an instance of this class
  21. *
  22. * @return TitleBlacklist
  23. */
  24. public static function singleton() {
  25. static $instance = null;
  26. if ( $instance === null ) {
  27. $instance = new self;
  28. }
  29. return $instance;
  30. }
  31. /**
  32. * Load all configured blacklist sources
  33. */
  34. public function load() {
  35. global $wgTitleBlacklistSources, $wgMemc, $wgTitleBlacklistCaching;
  36. wfProfileIn( __METHOD__ );
  37. //Try to find something in the cache
  38. $cachedBlacklist = $wgMemc->get( wfMemcKey( "title_blacklist_entries" ) );
  39. if( is_array( $cachedBlacklist ) && count( $cachedBlacklist ) > 0 && ( $cachedBlacklist[0]->getFormatVersion() == self::VERSION ) ) {
  40. $this->mBlacklist = $cachedBlacklist;
  41. wfProfileOut( __METHOD__ );
  42. return;
  43. }
  44. $sources = $wgTitleBlacklistSources;
  45. $sources[] = array( 'type' => TBLSRC_MSG );
  46. $this->mBlacklist = array();
  47. foreach( $sources as $source ) {
  48. $this->mBlacklist = array_merge( $this->mBlacklist, $this->parseBlacklist( $this->getBlacklistText( $source ) ) );
  49. }
  50. $wgMemc->set( wfMemcKey( "title_blacklist_entries" ), $this->mBlacklist, $wgTitleBlacklistCaching['expiry'] );
  51. wfProfileOut( __METHOD__ );
  52. }
  53. /**
  54. * Load all configured whitelist sources
  55. */
  56. public function loadWhitelist() {
  57. global $wgMemc, $wgTitleBlacklistCaching;
  58. wfProfileIn( __METHOD__ );
  59. $cachedWhitelist = $wgMemc->get( wfMemcKey( "title_whitelist_entries" ) );
  60. if( is_array( $cachedWhitelist ) && count( $cachedWhitelist ) > 0 && ( $cachedWhitelist[0]->getFormatVersion() != self::VERSION ) ) {
  61. $this->mWhitelist = $cachedWhitelist;
  62. wfProfileOut( __METHOD__ );
  63. return;
  64. }
  65. $this->mWhitelist = $this->parseBlacklist( wfMsgForContent( 'titlewhitelist' ) );
  66. $wgMemc->set( wfMemcKey( "title_whitelist_entries" ), $this->mWhitelist, $wgTitleBlacklistCaching['expiry'] );
  67. wfProfileOut( __METHOD__ );
  68. }
  69. /**
  70. * Get the text of a blacklist from a specified source
  71. *
  72. * @param $source A blacklist source from $wgTitleBlacklistSources
  73. * @return The content of the blacklist source as a string
  74. */
  75. private static function getBlacklistText( $source ) {
  76. if( !is_array( $source ) || count( $source ) <= 0 ) {
  77. return ''; //Return empty string in error case
  78. }
  79. if( $source['type'] == TBLSRC_MSG ) {
  80. return wfMsgForContent( 'titleblacklist' );
  81. } elseif( $source['type'] == TBLSRC_LOCALPAGE && count( $source ) >= 2 ) {
  82. $title = Title::newFromText( $source['src'] );
  83. if( is_null( $title ) ) {
  84. return '';
  85. }
  86. if( $title->getNamespace() == NS_MEDIAWIKI ) { //Use wfMsgForContent() for getting messages
  87. $msg = wfMsgForContent( $title->getText() );
  88. if( !wfEmptyMsg( 'titleblacklist', $msg ) ) {
  89. return $msg;
  90. } else {
  91. return '';
  92. }
  93. } else {
  94. $article = new Article( $title );
  95. if( $article->exists() ) {
  96. $article->followRedirect();
  97. return $article->getContent();
  98. }
  99. }
  100. } elseif( $source['type'] == TBLSRC_URL && count( $source ) >= 2 ) {
  101. return self::getHttp( $source['src'] );
  102. } elseif( $source['type'] == TBLSRC_FILE && count( $source ) >= 2 ) {
  103. if( file_exists( $source['src'] ) ) {
  104. return file_get_contents( $source['src'] );
  105. } else {
  106. return '';
  107. }
  108. }
  109. return '';
  110. }
  111. /**
  112. * Parse blacklist from a string
  113. *
  114. * @param $list Text of a blacklist source, as a string
  115. * @return An array of TitleBlacklistEntry entries
  116. */
  117. public static function parseBlacklist( $list ) {
  118. wfProfileIn( __METHOD__ );
  119. $lines = preg_split( "/\r?\n/", $list );
  120. $result = array();
  121. foreach ( $lines as $line ) {
  122. $line = TitleBlacklistEntry :: newFromString( $line );
  123. if ( $line ) {
  124. $result[] = $line;
  125. }
  126. }
  127. wfProfileOut( __METHOD__ );
  128. return $result;
  129. }
  130. /**
  131. * Check whether the blacklist restricts giver nuser
  132. * performing a specific action on the given Title
  133. *
  134. * @param $title Title to check
  135. * @param $user User to check
  136. * @param $action string Action to check; 'edit' if unspecified
  137. * @param $override bool If set to true, overrides work
  138. * @return TitleBlacklistEntry|false The corresponding TitleBlacklistEntry if blacklisted;
  139. * otherwise FALSE
  140. */
  141. public function userCannot( $title, $user, $action = 'edit', $override = true ) {
  142. if( $override && self::userCanOverride( $action ) ) {
  143. return false;
  144. } else {
  145. return $this->isBlacklisted( $title, $action );
  146. }
  147. }
  148. /**
  149. * Check whether the blacklist restricts
  150. * performing a specific action on the given Title
  151. *
  152. * @param $title Title to check
  153. * @param $action string Action to check; 'edit' if unspecified
  154. * @return TitleBlacklistEntry|false The corresponding TitleBlacklistEntry if blacklisted;
  155. * otherwise FALSE
  156. */
  157. public function isBlacklisted( $title, $action = 'edit' ) {
  158. if( !($title instanceof Title) ) {
  159. $title = Title::newFromText( $title );
  160. }
  161. $blacklist = $this->getBlacklist();
  162. foreach ( $blacklist as $item ) {
  163. if( $item->matches( $title, $action ) ) {
  164. if( $this->isWhitelisted( $title, $action ) ) {
  165. return false;
  166. }
  167. return $item; // "returning true"
  168. }
  169. }
  170. return false;
  171. }
  172. /**
  173. * Check whether it has been explicitly whitelisted that the
  174. * current User may perform a specific action on the given Title
  175. *
  176. * @param $title Title to check
  177. * @param $action string Action to check; 'edit' if unspecified
  178. * @return bool TRUE if whitelisted; otherwise FALSE
  179. */
  180. public function isWhitelisted( $title, $action = 'edit' ) {
  181. if( !($title instanceof Title) ) {
  182. $title = Title::newFromText( $title );
  183. }
  184. $whitelist = $this->getWhitelist();
  185. foreach( $whitelist as $item ) {
  186. if( $item->matches( $title, $action ) ) {
  187. return true;
  188. }
  189. }
  190. return false;
  191. }
  192. /**
  193. * Get the current blacklist
  194. *
  195. * @return Array of TitleBlacklistEntry items
  196. */
  197. public function getBlacklist() {
  198. if( is_null( $this->mBlacklist ) ) {
  199. $this->load();
  200. }
  201. return $this->mBlacklist;
  202. }
  203. /**
  204. * Get the current whitelist
  205. *
  206. * @return Array of TitleBlacklistEntry items
  207. */
  208. public function getWhitelist() {
  209. if( is_null( $this->mWhitelist ) ) {
  210. $this->loadWhitelist();
  211. }
  212. return $this->mWhitelist;
  213. }
  214. /**
  215. * Get the text of a blacklist source via HTTP
  216. *
  217. * @param $url string URL of the blacklist source
  218. * @return string The content of the blacklist source as a string
  219. */
  220. private static function getHttp( $url ) {
  221. global $messageMemc, $wgTitleBlacklistCaching;
  222. $key = "title_blacklist_source:" . md5( $url ); // Global shared
  223. $warnkey = wfMemcKey( "titleblacklistwarning", md5( $url ) );
  224. $result = $messageMemc->get( $key );
  225. $warn = $messageMemc->get( $warnkey );
  226. if ( !is_string( $result ) || ( !$warn && !mt_rand( 0, $wgTitleBlacklistCaching['warningchance'] ) ) ) {
  227. $result = Http::get( $url );
  228. $messageMemc->set( $warnkey, 1, $wgTitleBlacklistCaching['warningexpiry'] );
  229. $messageMemc->set( $key, $result, $wgTitleBlacklistCaching['expiry'] );
  230. }
  231. return $result;
  232. }
  233. /**
  234. * Invalidate the blacklist cache
  235. */
  236. public function invalidate() {
  237. global $wgMemc;
  238. $wgMemc->delete( wfMemcKey( "title_blacklist_entries" ) );
  239. }
  240. /**
  241. * Validate a new blacklist
  242. *
  243. * @param $blacklist array
  244. * @return Array of bad entries; empty array means blacklist is valid
  245. */
  246. public function validate( $blacklist ) {
  247. $badEntries = array();
  248. foreach( $blacklist as $e ) {
  249. wfSuppressWarnings();
  250. $regex = $e->getRegex();
  251. if( preg_match( "/{$regex}/u", '' ) === false ) {
  252. $badEntries[] = $e->getRaw();
  253. }
  254. wfRestoreWarnings();
  255. }
  256. return $badEntries;
  257. }
  258. /**
  259. * Inidcates whether user can override blacklist on certain action.
  260. *
  261. * @param $action Action
  262. *
  263. * @return bool
  264. */
  265. public static function userCanOverride( $action ) {
  266. global $wgUser;
  267. return $wgUser->isAllowed( 'tboverride' ) ||
  268. ( $action == 'new-account' && $wgUser->isAllowed( 'tboverride-account' ) );
  269. }
  270. }
  271. /**
  272. * Represents a title blacklist entry
  273. */
  274. class TitleBlacklistEntry {
  275. private
  276. $mRaw, ///< Raw line
  277. $mRegex, ///< Regular expression to match
  278. $mParams, ///< Parameters for this entry
  279. $mFormatVersion; ///< Entry format version
  280. /**
  281. * Construct a new TitleBlacklistEntry.
  282. *
  283. * @param $regex Regular expression to match
  284. * @param $params Parameters for this entry
  285. * @param $raw Raw contents of this line
  286. */
  287. private function __construct( $regex, $params, $raw ) {
  288. $this->mRaw = $raw;
  289. $this->mRegex = $regex;
  290. $this->mParams = $params;
  291. $this->mFormatVersion = TitleBlacklist::VERSION;
  292. }
  293. /**
  294. * Check whether a user can perform the specified action
  295. * on the specified Title
  296. *
  297. * @param $title Title to check
  298. * @param $action %Action to check
  299. * @return bool TRUE if the the regex matches the title, and is not overridden
  300. * else false if it doesn't match (or was overridden)
  301. */
  302. public function matches( $title, $action ) {
  303. wfSuppressWarnings();
  304. $match = preg_match( "/^(?:{$this->mRegex})$/us" . ( isset( $this->mParams['casesensitive'] ) ? '' : 'i' ), $title->getFullText() );
  305. wfRestoreWarnings();
  306. global $wgUser;
  307. if( $match ) {
  308. if( isset( $this->mParams['autoconfirmed'] ) && $wgUser->isAllowed( 'autoconfirmed' ) ) {
  309. return false;
  310. }
  311. if( isset( $this->mParams['moveonly'] ) && $action != 'move' ) {
  312. return false;
  313. }
  314. if( isset( $this->mParams['newaccountonly'] ) && $action != 'new-account' ) {
  315. return false;
  316. }
  317. if( !isset( $this->mParams['noedit'] ) && $action == 'edit' ) {
  318. return false;
  319. }
  320. if ( isset( $this->mParams['reupload'] ) && $action == 'upload' ) {
  321. // Special:Upload also checks 'create' permissions when not reuploading
  322. return false;
  323. }
  324. return true;
  325. }
  326. return false;
  327. }
  328. /**
  329. * Create a new TitleBlacklistEntry from a line of text
  330. *
  331. * @param $line String containing a line of blacklist text
  332. * @return A new corresponding TitleBlacklistEntry
  333. */
  334. public static function newFromString( $line ) {
  335. $raw = $line; // Keep line for raw data
  336. $options = array();
  337. // Strip comments
  338. $line = preg_replace( "/^\\s*([^#]*)\\s*((.*)?)$/", "\\1", $line );
  339. $line = trim( $line );
  340. // Parse the rest of message
  341. preg_match( '/^(.*?)(\s*<([^<>]*)>)?$/', $line, $pockets );
  342. @list( $full, $regex, $null, $opts_str ) = $pockets;
  343. $regex = trim( $regex );
  344. $regex = str_replace( '_', ' ', $regex ); // We'll be matching against text form
  345. $opts_str = trim( $opts_str );
  346. // Parse opts
  347. $opts = preg_split( '/\s*\|\s*/', $opts_str );
  348. foreach( $opts as $opt ) {
  349. $opt2 = strtolower( $opt );
  350. if( $opt2 == 'autoconfirmed' ) {
  351. $options['autoconfirmed'] = true;
  352. }
  353. if( $opt2 == 'moveonly' ) {
  354. $options['moveonly'] = true;
  355. }
  356. if( $opt2 == 'newaccountonly' ) {
  357. $options['newaccountonly'] = true;
  358. }
  359. if( $opt2 == 'noedit' ) {
  360. $options['noedit'] = true;
  361. }
  362. if( $opt2 == 'casesensitive' ) {
  363. $options['casesensitive'] = true;
  364. }
  365. if( $opt2 == 'reupload' ) {
  366. $options['reupload'] = true;
  367. }
  368. if( preg_match( '/errmsg\s*=\s*(.+)/i', $opt, $matches ) ) {
  369. $options['errmsg'] = $matches[1];
  370. }
  371. }
  372. // Process magic words
  373. preg_match_all( '/{{\s*([a-z]+)\s*:\s*(.+?)\s*}}/', $regex, $magicwords, PREG_SET_ORDER );
  374. foreach( $magicwords as $mword ) {
  375. global $wgParser; // Functions we're calling don't need, nevertheless let's use it
  376. switch( strtolower( $mword[1] ) ) {
  377. case 'ns':
  378. $cpf_result = CoreParserFunctions::ns( $wgParser, $mword[2] );
  379. if( is_string( $cpf_result ) ) {
  380. $regex = str_replace( $mword[0], $cpf_result, $regex ); // All result will have the same value, so we can just use str_seplace()
  381. }
  382. break;
  383. case 'int':
  384. $cpf_result = wfMsgForContent( $mword[2] );
  385. if( is_string( $cpf_result ) ) {
  386. $regex = str_replace( $mword[0], $cpf_result, $regex );
  387. }
  388. }
  389. }
  390. // Return result
  391. if( $regex ) {
  392. return new TitleBlacklistEntry( $regex, $options, $raw );
  393. } else {
  394. return null;
  395. }
  396. }
  397. /**
  398. * @return This entry's regular expression
  399. */
  400. public function getRegex() {
  401. return $this->mRegex;
  402. }
  403. /**
  404. * @returns This entry's raw line
  405. */
  406. public function getRaw() {
  407. return $this->mRaw;
  408. }
  409. /**
  410. * @return This entry's options
  411. */
  412. public function getOptions() {
  413. return $this->mOptions;
  414. }
  415. /**
  416. * @return Custom message for this entry
  417. */
  418. public function getCustomMessage() {
  419. return isset( $this->mParams['errmsg'] ) ? $this->mParams['errmsg'] : null;
  420. }
  421. /**
  422. * @return The format version
  423. */
  424. public function getFormatVersion() { return $this->mFormatVersion; }
  425. /**
  426. * Set the format version
  427. *
  428. * @param $v New version to set
  429. */
  430. public function setFormatVersion( $v ) { $this->mFormatVersion = $v; }
  431. /**
  432. * Return the error message name for the blacklist entry.
  433. *
  434. * @param $operation Operation name (as in titleblacklist-forbidden message name)
  435. *
  436. * @return The error message name
  437. */
  438. public function getErrorMessage( $operation ) {
  439. $message = $this->getCustomMessage();
  440. return $message ? $message : "titleblacklist-forbidden-{$operation}";
  441. }
  442. }
  443. //@}