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

/extensions/AbuseFilter/AbuseFilterVariableHolder.php

https://github.com/ChuguluGames/mediawiki-svn
PHP | 426 lines | 371 code | 39 blank | 16 comment | 19 complexity | 3efa3fce7316a9ef55f3cffb1fba23b2 MD5 | raw file
  1. <?php
  2. class AbuseFilterVariableHolder {
  3. var $mVars = array();
  4. static $varBlacklist = array( 'context' );
  5. function setVar( $variable, $datum ) {
  6. $variable = strtolower( $variable );
  7. if ( !( $datum instanceof AFPData || $datum instanceof AFComputedVariable ) ) {
  8. $datum = AFPData::newFromPHPVar( $datum );
  9. }
  10. $this->mVars[$variable] = $datum;
  11. }
  12. function setLazyLoadVar( $variable, $method, $parameters ) {
  13. $placeholder = new AFComputedVariable( $method, $parameters );
  14. $this->setVar( $variable, $placeholder );
  15. }
  16. function getVar( $variable ) {
  17. $variable = strtolower( $variable );
  18. if ( isset( $this->mVars[$variable] ) ) {
  19. if ( $this->mVars[$variable] instanceof AFComputedVariable ) {
  20. $value = $this->mVars[$variable]->compute( $this );
  21. $this->setVar( $variable, $value );
  22. return $value;
  23. } elseif ( $this->mVars[$variable] instanceof AFPData ) {
  24. return $this->mVars[$variable];
  25. }
  26. } else {
  27. return new AFPData();
  28. }
  29. }
  30. static function merge() {
  31. $newHolder = new AbuseFilterVariableHolder;
  32. foreach ( func_get_args() as $addHolder ) {
  33. $newHolder->addHolder( $addHolder );
  34. }
  35. return $newHolder;
  36. }
  37. function addHolder( $addHolder ) {
  38. if ( !is_object( $addHolder ) ) {
  39. throw new MWException( 'Invalid argument to AbuseFilterVariableHolder::addHolder' );
  40. }
  41. $this->mVars = array_merge( $this->mVars, $addHolder->mVars );
  42. }
  43. function __wakeup() {
  44. // Reset the context.
  45. $this->setVar( 'context', 'stored' );
  46. }
  47. function exportAllVars() {
  48. $allVarNames = array_keys( $this->mVars );
  49. $exported = array();
  50. foreach ( $allVarNames as $varName ) {
  51. if ( !in_array( $varName, self::$varBlacklist ) ) {
  52. $exported[$varName] = $this->getVar( $varName )->toString();
  53. }
  54. }
  55. return $exported;
  56. }
  57. function varIsSet( $var ) {
  58. return array_key_exists( $var, $this->mVars );
  59. }
  60. /**
  61. * Compute all vars which need DB access. Useful for vars which are going to be saved
  62. * cross-wiki or used for offline analysis.
  63. */
  64. function computeDBVars() {
  65. static $dbTypes = array(
  66. 'links-from-wikitext-or-database',
  67. 'load-recent-authors',
  68. 'get-page-restrictions',
  69. 'simple-user-accessor',
  70. 'user-age',
  71. 'user-groups',
  72. 'revision-text-by-id',
  73. 'revision-text-by-timestamp'
  74. );
  75. foreach ( $this->mVars as $name => $value ) {
  76. if ( $value instanceof AFComputedVariable &&
  77. in_array( $value->mMethod, $dbTypes ) ) {
  78. $value = $value->compute( $this );
  79. $this->setVar( $name, $value );
  80. }
  81. }
  82. }
  83. }
  84. class AFComputedVariable {
  85. var $mMethod, $mParameters;
  86. static $userCache = array();
  87. static $articleCache = array();
  88. function __construct( $method, $parameters ) {
  89. $this->mMethod = $method;
  90. $this->mParameters = $parameters;
  91. }
  92. /**
  93. * It's like Article::prepareTextForEdit, but not for editing (old wikitext usually)
  94. *
  95. *
  96. * @param $wikitext String
  97. * @param $article Article
  98. *
  99. * @return object
  100. */
  101. function parseNonEditWikitext( $wikitext, $article ) {
  102. static $cache = array();
  103. $cacheKey = md5( $wikitext ) . ':' . $article->mTitle->getPrefixedText();
  104. if ( isset( $cache[$cacheKey] ) ) {
  105. return $cache[$cacheKey];
  106. }
  107. global $wgParser;
  108. $edit = (object)array();
  109. $options = new ParserOptions;
  110. $options->setTidy( true );
  111. $edit->output = $wgParser->parse( $wikitext, $article->getTitle(), $options );
  112. $cache[$cacheKey] = $edit;
  113. return $edit;
  114. }
  115. static function userObjectFromName( $username ) {
  116. if ( isset( self::$userCache[$username] ) ) {
  117. return self::$userCache[$username];
  118. }
  119. wfDebug( "Couldn't find user $username in cache\n" );
  120. if ( count( self::$userCache ) > 1000 ) {
  121. self::$userCache = array();
  122. }
  123. if ( IP::isIPAddress( $username ) ) {
  124. $u = new User;
  125. $u->setName( $username );
  126. self::$userCache[$username] = $u;
  127. return $u;
  128. }
  129. $user = User::newFromName( $username );
  130. $user->load();
  131. self::$userCache[$username] = $user;
  132. return $user;
  133. }
  134. static function articleFromTitle( $namespace, $title ) {
  135. if ( isset( self::$articleCache["$namespace:$title"] ) ) {
  136. return self::$articleCache["$namespace:$title"];
  137. }
  138. if ( count( self::$articleCache ) > 1000 ) {
  139. self::$articleCache = array();
  140. }
  141. wfDebug( "Creating article object for $namespace:$title in cache\n" );
  142. $t = Title::makeTitle( $namespace, $title );
  143. self::$articleCache["$namespace:$title"] = new Article( $t );
  144. return self::$articleCache["$namespace:$title"];
  145. }
  146. static function getLinksFromDB( $article ) {
  147. // Stolen from ConfirmEdit
  148. $id = $article->getId();
  149. if ( !$id ) {
  150. return array();
  151. }
  152. $dbr = wfGetDB( DB_SLAVE );
  153. $res = $dbr->select(
  154. 'externallinks',
  155. array( 'el_to' ),
  156. array( 'el_from' => $id ),
  157. __METHOD__
  158. );
  159. $links = array();
  160. foreach( $res as $row ) {
  161. $links[] = $row->el_to;
  162. }
  163. return $links;
  164. }
  165. function compute( $vars ) {
  166. $parameters = $this->mParameters;
  167. $result = null;
  168. switch( $this->mMethod ) {
  169. case 'diff':
  170. $text1Var = $parameters['oldtext-var'];
  171. $text2Var = $parameters['newtext-var'];
  172. $text1 = $vars->getVar( $text1Var )->toString();
  173. $text2 = $vars->getVar( $text2Var )->toString();
  174. $result = wfDiff( $text1, $text2 );
  175. $result = trim( preg_replace( "/^\\\\ No newline at end of file\n/m", '', $result ) );
  176. break;
  177. case 'diff-split':
  178. $diff = $vars->getVar( $parameters['diff-var'] )->toString();
  179. $line_prefix = $parameters['line-prefix'];
  180. $diff_lines = explode( "\n", $diff );
  181. $interest_lines = array();
  182. foreach ( $diff_lines as $line ) {
  183. if ( substr( $line, 0, 1 ) === $line_prefix ) {
  184. $interest_lines[] = substr( $line, strlen( $line_prefix ) );
  185. }
  186. }
  187. $result = $interest_lines;
  188. break;
  189. case 'links-from-wikitext':
  190. // This should ONLY be used when sharing a parse operation with the edit.
  191. global $wgArticle;
  192. $article = self::articleFromTitle(
  193. $parameters['namespace'],
  194. $parameters['title']
  195. );
  196. if ( $wgArticle && $article->getTitle()->equals( $wgArticle->getTitle() ) ) {
  197. $textVar = $parameters['text-var'];
  198. $new_text = $vars->getVar( $textVar )->toString();
  199. $editInfo = $article->prepareTextForEdit( $new_text );
  200. $links = array_keys( $editInfo->output->getExternalLinks() );
  201. $result = $links;
  202. } else {
  203. // Change to links-from-wikitext-nonedit.
  204. $this->mMethod = 'links-from-wikitext-nonedit';
  205. $result = $this->compute( $vars );
  206. }
  207. break;
  208. case 'links-from-wikitext-nonedit':
  209. case 'links-from-wikitext-or-database':
  210. $article = self::articleFromTitle(
  211. $parameters['namespace'],
  212. $parameters['title']
  213. );
  214. if ( $vars->getVar( 'context' )->toString() == 'filter' ) {
  215. $links = $this->getLinksFromDB( $article );
  216. wfDebug( "AbuseFilter: loading old links from DB\n" );
  217. } else {
  218. wfDebug( "AbuseFilter: loading old links from Parser\n" );
  219. $textVar = $parameters['text-var'];
  220. $wikitext = $vars->getVar( $textVar )->toString();
  221. $editInfo = $this->parseNonEditWikitext( $wikitext, $article );
  222. $links = array_keys( $editInfo->output->getExternalLinks() );
  223. }
  224. $result = $links;
  225. break;
  226. case 'link-diff-added':
  227. case 'link-diff-removed':
  228. $oldLinkVar = $parameters['oldlink-var'];
  229. $newLinkVar = $parameters['newlink-var'];
  230. $oldLinks = $vars->getVar( $oldLinkVar )->toString();
  231. $newLinks = $vars->getVar( $newLinkVar )->toString();
  232. $oldLinks = explode( "\n", $oldLinks );
  233. $newLinks = explode( "\n", $newLinks );
  234. if ( $this->mMethod == 'link-diff-added' ) {
  235. $result = array_diff( $newLinks, $oldLinks );
  236. }
  237. if ( $this->mMethod == 'link-diff-removed' ) {
  238. $result = array_diff( $oldLinks, $newLinks );
  239. }
  240. break;
  241. case 'parse-wikitext':
  242. // Should ONLY be used when sharing a parse operation with the edit.
  243. global $wgArticle;
  244. $article = self::articleFromTitle( $parameters['namespace'], $parameters['title'] );
  245. if ( $wgArticle && $article->getTitle() === $wgArticle->getTitle() ) {
  246. $textVar = $parameters['wikitext-var'];
  247. $new_text = $vars->getVar( $textVar )->toString();
  248. $editInfo = $article->prepareTextForEdit( $new_text );
  249. $newHTML = $editInfo->output->getText();
  250. // Kill the PP limit comments. Ideally we'd just remove these by not setting the
  251. // parser option, but then we can't share a parse operation with the edit, which is bad.
  252. $result = preg_replace( '/<!--\s*NewPP limit report[^>]*-->\s*$/si', '', $newHTML );
  253. } else {
  254. // Change to parse-wikitext-nonedit.
  255. $this->mMethod = 'parse-wikitext-nonedit';
  256. $result = $this->compute( $vars );
  257. }
  258. break;
  259. case 'parse-wikitext-nonedit':
  260. $article = self::articleFromTitle( $parameters['namespace'], $parameters['title'] );
  261. $textVar = $parameters['wikitext-var'];
  262. $text = $vars->getVar( $textVar )->toString();
  263. $editInfo = $this->parseNonEditWikitext( $text, $article );
  264. $result = $editInfo->output->getText();
  265. break;
  266. case 'strip-html':
  267. $htmlVar = $parameters['html-var'];
  268. $html = $vars->getVar( $htmlVar )->toString();
  269. $result = StringUtils::delimiterReplace( '<', '>', '', $html );
  270. break;
  271. case 'load-recent-authors':
  272. $cutOff = $parameters['cutoff'];
  273. $title = Title::makeTitle( $parameters['namespace'], $parameters['title'] );
  274. if ( !$title->exists() ) {
  275. $result = '';
  276. break;
  277. }
  278. $dbr = wfGetDB( DB_SLAVE );
  279. $res = $dbr->select( 'revision',
  280. 'DISTINCT rev_user_text',
  281. array(
  282. 'rev_page' => $title->getArticleId(),
  283. 'rev_timestamp<' . $dbr->addQuotes( $dbr->timestamp( $cutOff ) )
  284. ),
  285. __METHOD__,
  286. array( 'ORDER BY' => 'rev_timestamp DESC', 'LIMIT' => 10 )
  287. );
  288. $users = array();
  289. foreach( $res as $row ) {
  290. $users[] = $row->rev_user_text;
  291. }
  292. $result = $users;
  293. break;
  294. case 'get-page-restrictions':
  295. $action = $parameters['action'];
  296. $title = Title::makeTitle( $parameters['namespace'], $parameters['title'] );
  297. $rights = $title->getRestrictions( $action );
  298. $rights = count( $rights ) ? $rights : array();
  299. $result = $rights;
  300. break;
  301. case 'simple-user-accessor':
  302. $user = $parameters['user'];
  303. $method = $parameters['method'];
  304. if ( !$user ) {
  305. throw new MWException( 'No user parameter given.' );
  306. }
  307. $obj = self::userObjectFromName( $user );
  308. if ( !$obj ) {
  309. throw new MWException( "Invalid username $user" );
  310. }
  311. $result = call_user_func( array( $obj, $method ) );
  312. break;
  313. case 'user-age':
  314. $user = $parameters['user'];
  315. $asOf = $parameters['asof'];
  316. $obj = self::userObjectFromName( $user );
  317. if ( $obj->getId() == 0 ) {
  318. $result = 0;
  319. break;
  320. }
  321. $registration = $obj->getRegistration();
  322. $result =
  323. wfTimestamp( TS_UNIX, $asOf ) -
  324. wfTimestampOrNull( TS_UNIX, $registration );
  325. break;
  326. case 'user-groups':
  327. $user = $parameters['user'];
  328. $obj = self::userObjectFromName( $user );
  329. $result = $obj->getEffectiveGroups();
  330. break;
  331. case 'length':
  332. $s = $vars->getVar( $parameters['length-var'] )->toString();
  333. $result = strlen( $s );
  334. break;
  335. case 'subtract':
  336. $v1 = $vars->getVar( $parameters['val1-var'] )->toFloat();
  337. $v2 = $vars->getVar( $parameters['val2-var'] )->toFloat();
  338. $result = $v1 - $v2;
  339. break;
  340. case 'revision-text-by-id':
  341. $rev = Revision::newFromId( $parameters['revid'] );
  342. $result = $rev->getText();
  343. break;
  344. case 'revision-text-by-timestamp':
  345. $timestamp = $parameters['timestamp'];
  346. $title = Title::makeTitle( $parameters['namespace'], $parameters['title'] );
  347. $dbr = wfGetDB( DB_SLAVE );
  348. $rev = Revision::loadFromTimestamp( $dbr, $title, $timestamp );
  349. if ( $rev ) {
  350. $result = $rev->getText();
  351. } else {
  352. $result = '';
  353. }
  354. break;
  355. default:
  356. if ( wfRunHooks( 'AbuseFilter-computeVariable',
  357. array( $this->mMethod, $vars ) ) ) {
  358. throw new AFPException( 'Unknown variable compute type ' . $this->mMethod );
  359. }
  360. }
  361. return $result instanceof AFPData
  362. ? $result : AFPData::newFromPHPVar( $result );
  363. }
  364. }