PageRenderTime 44ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/extensions/EditSimilar/EditSimilar.class.php

https://github.com/ChuguluGames/mediawiki-svn
PHP | 372 lines | 221 code | 44 blank | 107 comment | 33 complexity | e6919e2e0d2bea8339aa5de88e3fb567 MD5 | raw file
  1. <?php
  2. /**
  3. * How this extension works:
  4. * - upon save, the script searches for articles that are similar
  5. * right now, I have assumed the following criteria:
  6. * ** articles that need attention
  7. * ** articles similar in category to the one we edited
  8. * ** if no similar articles were found, we're taking results straight from
  9. * categories that need attention
  10. * ** number of articles in result is limited
  11. *
  12. * IMPORTANT NOTE: This extension REQUIRES the article
  13. * MediaWiki:EditSimilar-Categories to exist on your wiki in order to run.
  14. * If this article is nonexistent, the extension will disable itself.
  15. *
  16. * Format of the article is as follows:
  17. * * Chosen Stub Category 1
  18. * * Chosen Stub Category 2
  19. * etc. (separated by stars)
  20. *
  21. * Insert '-' if you want to disable the extension without blanking the
  22. * commanding article.
  23. *
  24. * @file
  25. */
  26. class EditSimilar {
  27. var $mBaseArticle; // the article from which we hail in our quest for similiarities, this is its title
  28. /**
  29. * @var String: how do we mark articles that need attention? Currently, by
  30. * category only
  31. */
  32. var $mMarkerType;
  33. /**
  34. * @var Array: the marker array (for now it contains categories)
  35. */
  36. var $mAttentionMarkers;
  37. /**
  38. * @var Integer: limit up the pool of 'stubs' to choose from, controlled
  39. * via the $wgEditSimilarMaxResultsPool global variable
  40. */
  41. var $mPoolLimit;
  42. /**
  43. * @var Array: array of extracted categories that this saved article is in
  44. */
  45. var $mBaseCategories;
  46. /**
  47. * @var Boolean: to differentiate between really similar results or just
  48. * needing attention
  49. */
  50. var $mSimilarArticles;
  51. /**
  52. * Constructor
  53. *
  54. * @param $article Integer: article ID number
  55. * @param $markerType String: always 'category'
  56. */
  57. public function __construct( $article, $markerType = 'category' ) {
  58. global $wgEditSimilarMaxResultsPool;
  59. $this->mBaseArticle = $article;
  60. $this->mMarkerType = $markerType;
  61. $this->mAttentionMarkers = $this->getStubCategories();
  62. $this->mPoolLimit = $wgEditSimilarMaxResultsPool;
  63. $this->mBaseCategories = $this->getBaseCategories();
  64. $this->mSimilarArticles = true;
  65. }
  66. /**
  67. * Fetch categories marked as 'stub categories', controlled via the
  68. * MediaWiki:EditSimilar-Categories interface message.
  69. *
  70. * @return Array|Boolean: array of category names on success, false on
  71. * failure (if MediaWiki:EditSimilar-Categories is
  72. * empty or contains -)
  73. */
  74. function getStubCategories() {
  75. $stubCategories = wfMsgForContent( 'EditSimilar-Categories' );
  76. if (
  77. ( '&lt;EditSimilar-Categories&gt;' == $stubCategories ) ||
  78. ( '' == $stubCategories ) || ( '-' == $stubCategories )
  79. )
  80. {
  81. return false;
  82. } else {
  83. $lines = preg_split( '/\*/', $stubCategories );
  84. $normalisedLines = array();
  85. array_shift( $lines );
  86. foreach ( $lines as $line ) {
  87. $normalisedLines[] = str_replace( ' ', '_', trim( $line ) );
  88. }
  89. return $normalisedLines;
  90. }
  91. }
  92. /**
  93. * Main function that returns articles we deem similar or worth showing
  94. *
  95. * @return Array|Boolean: array of article names on success, false on
  96. * failure
  97. */
  98. function getSimilarArticles() {
  99. global $wgUser, $wgEditSimilarMaxResultsToDisplay;
  100. if ( empty( $this->mAttentionMarkers ) || !$this->mAttentionMarkers ) {
  101. return false;
  102. }
  103. $text = '';
  104. $articles = array();
  105. $x = 0;
  106. while (
  107. ( count( $articles ) < $wgEditSimilarMaxResultsToDisplay ) &&
  108. ( $x < count( $this->mAttentionMarkers ) )
  109. )
  110. {
  111. $articles = array_merge(
  112. $articles,
  113. $this->getResults( $this->mAttentionMarkers[$x] )
  114. );
  115. if ( !empty( $articles ) ) {
  116. $articles = array_unique( $articles );
  117. }
  118. $x++;
  119. }
  120. if ( empty( $articles ) ) {
  121. $articles = $this->getAdditionalCheck();
  122. // second check to make sure we have anything to display
  123. if ( empty( $articles ) ) {
  124. return false;
  125. }
  126. $articles = array_unique( $articles );
  127. $this->mSimilarArticles = false;
  128. }
  129. if ( count( $articles ) == 1 ) { // in this case, array_rand returns a single element, not an array
  130. $rand_articles = array( 0 );
  131. } else {
  132. $rand_articles = array_rand(
  133. $articles,
  134. min( $wgEditSimilarMaxResultsToDisplay, count( $articles ) )
  135. );
  136. }
  137. $sk = $wgUser->getSkin();
  138. $realRandValues = array();
  139. if ( empty( $rand_articles ) ) {
  140. return false;
  141. }
  142. $translatedTitles = array();
  143. foreach ( $rand_articles as $r_key => $rand_article_key ) {
  144. $translatedTitles[] = $articles[$rand_article_key];
  145. }
  146. $translatedTitles = $this->idsToTitles( $translatedTitles );
  147. foreach ( $translatedTitles as $linkTitle ) {
  148. $articleLink = $sk->makeKnownLinkObj( $linkTitle );
  149. $realRandValues[] = $articleLink;
  150. }
  151. return $realRandValues;
  152. }
  153. /**
  154. * Extract all categories our base article is in
  155. *
  156. * @return Array|Boolean: array of category names on success, false on
  157. * failure
  158. */
  159. function getBaseCategories() {
  160. global $wgEditSimilarMaxResultsToDisplay;
  161. if ( empty( $this->mAttentionMarkers ) || !$this->mAttentionMarkers ) {
  162. return false;
  163. }
  164. $dbr = wfGetDB( DB_SLAVE );
  165. $resultArray = array();
  166. $res = $dbr->select(
  167. array( 'categorylinks' ),
  168. array( 'cl_to' ),
  169. array( 'cl_from' => $this->mBaseArticle ),
  170. __METHOD__,
  171. array(
  172. 'ORDER_BY' => 'cl_from',
  173. 'USE_INDEX' => 'cl_from'
  174. )
  175. );
  176. foreach( $res as $x ) {
  177. if ( !in_array( $x->cl_to, $this->mAttentionMarkers ) ) {
  178. $resultArray[] = $x->cl_to;
  179. }
  180. }
  181. if ( !empty( $resultArray ) ) {
  182. return $resultArray;
  183. } else {
  184. return false;
  185. }
  186. }
  187. /**
  188. * Latest addition: if we got no results at all (indicating that:
  189. * A - the article had no categories,
  190. * B - the article had no relevant results for its categories)
  191. *
  192. * This is to ensure we can get always (well, almost - if "marker"
  193. * categories get no results, it's dead in the water anyway) some results.
  194. *
  195. * @return Array: array of category names
  196. */
  197. function getAdditionalCheck() {
  198. $dbr = wfGetDB( DB_SLAVE );
  199. $fixedNames = array();
  200. foreach ( $this->mAttentionMarkers as $category ) {
  201. $fixedNames[] = $dbr->addQuotes( $category );
  202. }
  203. $stringedNames = implode( ',', $fixedNames );
  204. $res = $dbr->select(
  205. 'categorylinks',
  206. array( 'cl_from' ),
  207. array( "cl_to IN ($stringedNames)" ),
  208. __METHOD__
  209. );
  210. $resultArray = array();
  211. foreach( $res as $x ) {
  212. if ( $this->mBaseArticle != $x->cl_from ) {
  213. $resultArray[] = $x->cl_from;
  214. }
  215. }
  216. return $resultArray;
  217. }
  218. /**
  219. * Turn result IDs into Title objects in one query rather than multiple
  220. * ones.
  221. *
  222. * @param $idArray Array: array of page ID numbers
  223. * @return Array: array of Title objects
  224. */
  225. function idsToTitles( $idArray ) {
  226. global $wgContentNamespaces;
  227. $dbr = wfGetDB( DB_SLAVE );
  228. $stringedNames = implode( ',', $idArray );
  229. $res = $dbr->select(
  230. 'page',
  231. array( 'page_namespace', 'page_title' ),
  232. array( "page_id IN ($stringedNames)" ),
  233. __METHOD__
  234. );
  235. $resultArray = array();
  236. // so for now, to speed things up, just discard results from other namespaces (and subpages)
  237. while (
  238. ( $x = $dbr->fetchObject( $res ) ) &&
  239. ( in_array( $x->page_namespace, $wgContentNamespaces ) ) &&
  240. strpos( $x->page_title, '/' ) === false
  241. )
  242. {
  243. $resultArray[] = Title::makeTitle(
  244. $x->page_namespace,
  245. $x->page_title
  246. );
  247. }
  248. $dbr->freeResult( $res );
  249. return $resultArray;
  250. }
  251. /**
  252. * Get categories from the 'stub' or 'attention needed' category
  253. *
  254. * @param $markerCategory String: category name
  255. * @return Array: array of category names
  256. */
  257. function getResults( $markerCategory ) {
  258. $dbr = wfGetDB( DB_SLAVE );
  259. $title = Title::makeTitle( NS_CATEGORY, $markerCategory );
  260. $resultArray = array();
  261. if ( empty( $this->mBaseCategories ) ) {
  262. return $resultArray;
  263. }
  264. // @todo CHECKME: is it possible to make this query use MediaWiki's
  265. // Database functions? If so, rewrite it!
  266. $query = "SELECT c1.cl_from
  267. FROM {$dbr->tableName( 'categorylinks' )} AS c1, {$dbr->tableName( 'categorylinks' )} AS c2
  268. WHERE c1.cl_from = c2.cl_from
  269. AND c1.cl_to = " . $dbr->addQuotes( $title->getDBkey() ) . "
  270. AND c2.cl_to IN (";
  271. $fixedNames = array();
  272. foreach ( $this->mBaseCategories as $category ) {
  273. $fixedNames[] = $dbr->addQuotes( $category );
  274. }
  275. $stringed_names = implode( ',', $fixedNames );
  276. $query .= $stringed_names . ')';
  277. $res = $dbr->query( $query, __METHOD__ );
  278. foreach( $res as $x ) {
  279. if ( $this->mBaseArticle != $x->cl_from ) {
  280. $resultArray[] = $x->cl_from;
  281. }
  282. }
  283. return $resultArray;
  284. }
  285. /**
  286. * Message box wrapper
  287. *
  288. * @param $text String: message to show
  289. */
  290. public static function showMessage( $text ) {
  291. global $wgOut, $wgUser, $wgScript, $wgScriptPath;
  292. $wgOut->addExtensionStyle( $wgScriptPath . '/extensions/EditSimilar/EditSimilar.css' );
  293. // If the user is logged in, give them a link to their preferences in
  294. // case if they want to disable EditSimilar suggestions
  295. if ( $wgUser->isLoggedIn() ) {
  296. $link = '<div class="editsimilar_dismiss">[<span class="plainlinks"><a href="' .
  297. $wgScript . '?title=Special:Preferences#prefsection-4" id="editsimilar_preferences">' .
  298. wfMsg( 'editsimilar-link-disable' ) .
  299. '</a></span>]</div><div style="display:block">&#160;</div>';
  300. } else {
  301. $link = '';
  302. }
  303. $wgOut->addHTML(
  304. '<div id="editsimilar_links" class="usermessage editsimilar"><div>' .
  305. $text . '</div>' . $link . '</div>'
  306. );
  307. }
  308. /**
  309. * For determining whether to display the message or not
  310. *
  311. * @return Boolean: true to show the message, false to not show it
  312. */
  313. public static function checkCounter() {
  314. global $wgEditSimilarCounterValue;
  315. if ( isset( $_SESSION['ES_counter'] ) ) {
  316. $_SESSION['ES_counter']--;
  317. if ( $_SESSION['ES_counter'] > 0 ) {
  318. return false;
  319. } else {
  320. $_SESSION['ES_counter'] = $wgEditSimilarCounterValue;
  321. return true;
  322. }
  323. } else {
  324. $_SESSION['ES_counter'] = $wgEditSimilarCounterValue;
  325. return true;
  326. }
  327. }
  328. }