PageRenderTime 51ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

/includes/WatchlistEditor.php

https://github.com/tav/confluence
PHP | 494 lines | 319 code | 24 blank | 151 comment | 43 complexity | d91c343935d32b8fc4af40faefefd714 MD5 | raw file
Possible License(s): GPL-2.0, LGPL-3.0
  1. <?php
  2. /**
  3. * Provides the UI through which users can perform editing
  4. * operations on their watchlist
  5. *
  6. * @ingroup Watchlist
  7. * @author Rob Church <robchur@gmail.com>
  8. */
  9. class WatchlistEditor {
  10. /**
  11. * Editing modes
  12. */
  13. const EDIT_CLEAR = 1;
  14. const EDIT_RAW = 2;
  15. const EDIT_NORMAL = 3;
  16. /**
  17. * Main execution point
  18. *
  19. * @param $user User
  20. * @param $output OutputPage
  21. * @param $request WebRequest
  22. * @param $mode int
  23. */
  24. public function execute( $user, $output, $request, $mode ) {
  25. global $wgUser;
  26. if( wfReadOnly() ) {
  27. $output->readOnlyPage();
  28. return;
  29. }
  30. switch( $mode ) {
  31. case self::EDIT_CLEAR:
  32. // The "Clear" link scared people too much.
  33. // Pass on to the raw editor, from which it's very easy to clear.
  34. case self::EDIT_RAW:
  35. $output->setPageTitle( wfMsg( 'watchlistedit-raw-title' ) );
  36. if( $request->wasPosted() && $this->checkToken( $request, $wgUser ) ) {
  37. $wanted = $this->extractTitles( $request->getText( 'titles' ) );
  38. $current = $this->getWatchlist( $user );
  39. if( count( $wanted ) > 0 ) {
  40. $toWatch = array_diff( $wanted, $current );
  41. $toUnwatch = array_diff( $current, $wanted );
  42. $this->watchTitles( $toWatch, $user );
  43. $this->unwatchTitles( $toUnwatch, $user );
  44. $user->invalidateCache();
  45. if( count( $toWatch ) > 0 || count( $toUnwatch ) > 0 )
  46. $output->addHTML( wfMsgExt( 'watchlistedit-raw-done', 'parse' ) );
  47. if( ( $count = count( $toWatch ) ) > 0 ) {
  48. $output->addHTML( wfMsgExt( 'watchlistedit-raw-added', 'parse', $count ) );
  49. $this->showTitles( $toWatch, $output, $wgUser->getSkin() );
  50. }
  51. if( ( $count = count( $toUnwatch ) ) > 0 ) {
  52. $output->addHTML( wfMsgExt( 'watchlistedit-raw-removed', 'parse', $count ) );
  53. $this->showTitles( $toUnwatch, $output, $wgUser->getSkin() );
  54. }
  55. } else {
  56. $this->clearWatchlist( $user );
  57. $user->invalidateCache();
  58. $output->addHTML( wfMsgExt( 'watchlistedit-raw-removed', 'parse', count( $current ) ) );
  59. $this->showTitles( $current, $output, $wgUser->getSkin() );
  60. }
  61. }
  62. $this->showRawForm( $output, $user );
  63. break;
  64. case self::EDIT_NORMAL:
  65. $output->setPageTitle( wfMsg( 'watchlistedit-normal-title' ) );
  66. if( $request->wasPosted() && $this->checkToken( $request, $wgUser ) ) {
  67. $titles = $this->extractTitles( $request->getArray( 'titles' ) );
  68. $this->unwatchTitles( $titles, $user );
  69. $user->invalidateCache();
  70. $output->addHTML( wfMsgExt( 'watchlistedit-normal-done', 'parse',
  71. $GLOBALS['wgLang']->formatNum( count( $titles ) ) ) );
  72. $this->showTitles( $titles, $output, $wgUser->getSkin() );
  73. }
  74. $this->showNormalForm( $output, $user );
  75. }
  76. }
  77. /**
  78. * Check the edit token from a form submission
  79. *
  80. * @param $request WebRequest
  81. * @param $user User
  82. * @return bool
  83. */
  84. private function checkToken( $request, $user ) {
  85. return $user->matchEditToken( $request->getVal( 'token' ), 'watchlistedit' );
  86. }
  87. /**
  88. * Extract a list of titles from a blob of text, returning
  89. * (prefixed) strings; unwatchable titles are ignored
  90. *
  91. * @param $list mixed
  92. * @return array
  93. */
  94. private function extractTitles( $list ) {
  95. $titles = array();
  96. if( !is_array( $list ) ) {
  97. $list = explode( "\n", trim( $list ) );
  98. if( !is_array( $list ) )
  99. return array();
  100. }
  101. foreach( $list as $text ) {
  102. $text = trim( $text );
  103. if( strlen( $text ) > 0 ) {
  104. $title = Title::newFromText( $text );
  105. if( $title instanceof Title && $title->isWatchable() )
  106. $titles[] = $title->getPrefixedText();
  107. }
  108. }
  109. return array_unique( $titles );
  110. }
  111. /**
  112. * Print out a list of linked titles
  113. *
  114. * $titles can be an array of strings or Title objects; the former
  115. * is preferred, since Titles are very memory-heavy
  116. *
  117. * @param $titles An array of strings, or Title objects
  118. * @param $output OutputPage
  119. * @param $skin Skin
  120. */
  121. private function showTitles( $titles, $output, $skin ) {
  122. $talk = wfMsgHtml( 'talkpagelinktext' );
  123. // Do a batch existence check
  124. $batch = new LinkBatch();
  125. foreach( $titles as $title ) {
  126. if( !$title instanceof Title )
  127. $title = Title::newFromText( $title );
  128. if( $title instanceof Title ) {
  129. $batch->addObj( $title );
  130. $batch->addObj( $title->getTalkPage() );
  131. }
  132. }
  133. $batch->execute();
  134. // Print out the list
  135. $output->addHTML( "<ul>\n" );
  136. foreach( $titles as $title ) {
  137. if( !$title instanceof Title )
  138. $title = Title::newFromText( $title );
  139. if( $title instanceof Title ) {
  140. $output->addHTML( "<li>" . $skin->makeLinkObj( $title )
  141. . ' (' . $skin->makeLinkObj( $title->getTalkPage(), $talk ) . ")</li>\n" );
  142. }
  143. }
  144. $output->addHTML( "</ul>\n" );
  145. }
  146. /**
  147. * Count the number of titles on a user's watchlist, excluding talk pages
  148. *
  149. * @param $user User
  150. * @return int
  151. */
  152. private function countWatchlist( $user ) {
  153. $dbr = wfGetDB( DB_MASTER );
  154. $res = $dbr->select( 'watchlist', 'COUNT(*) AS count', array( 'wl_user' => $user->getId() ), __METHOD__ );
  155. $row = $dbr->fetchObject( $res );
  156. return ceil( $row->count / 2 ); // Paranoia
  157. }
  158. /**
  159. * Prepare a list of titles on a user's watchlist (excluding talk pages)
  160. * and return an array of (prefixed) strings
  161. *
  162. * @param $user User
  163. * @return array
  164. */
  165. private function getWatchlist( $user ) {
  166. $list = array();
  167. $dbr = wfGetDB( DB_MASTER );
  168. $res = $dbr->select(
  169. 'watchlist',
  170. '*',
  171. array(
  172. 'wl_user' => $user->getId(),
  173. ),
  174. __METHOD__
  175. );
  176. if( $res->numRows() > 0 ) {
  177. while( $row = $res->fetchObject() ) {
  178. $title = Title::makeTitleSafe( $row->wl_namespace, $row->wl_title );
  179. if( $title instanceof Title && !$title->isTalkPage() )
  180. $list[] = $title->getPrefixedText();
  181. }
  182. $res->free();
  183. }
  184. return $list;
  185. }
  186. /**
  187. * Get a list of titles on a user's watchlist, excluding talk pages,
  188. * and return as a two-dimensional array with namespace, title and
  189. * redirect status
  190. *
  191. * @param $user User
  192. * @return array
  193. */
  194. private function getWatchlistInfo( $user ) {
  195. $titles = array();
  196. $dbr = wfGetDB( DB_MASTER );
  197. $uid = intval( $user->getId() );
  198. list( $watchlist, $page ) = $dbr->tableNamesN( 'watchlist', 'page' );
  199. $sql = "SELECT wl_namespace, wl_title, page_id, page_len, page_is_redirect
  200. FROM {$watchlist} LEFT JOIN {$page} ON ( wl_namespace = page_namespace
  201. AND wl_title = page_title ) WHERE wl_user = {$uid}";
  202. $res = $dbr->query( $sql, __METHOD__ );
  203. if( $res && $dbr->numRows( $res ) > 0 ) {
  204. $cache = LinkCache::singleton();
  205. while( $row = $dbr->fetchObject( $res ) ) {
  206. $title = Title::makeTitleSafe( $row->wl_namespace, $row->wl_title );
  207. if( $title instanceof Title ) {
  208. // Update the link cache while we're at it
  209. if( $row->page_id ) {
  210. $cache->addGoodLinkObj( $row->page_id, $title, $row->page_len, $row->page_is_redirect );
  211. } else {
  212. $cache->addBadLinkObj( $title );
  213. }
  214. // Ignore non-talk
  215. if( !$title->isTalkPage() )
  216. $titles[$row->wl_namespace][$row->wl_title] = $row->page_is_redirect;
  217. }
  218. }
  219. }
  220. return $titles;
  221. }
  222. /**
  223. * Show a message indicating the number of items on the user's watchlist,
  224. * and return this count for additional checking
  225. *
  226. * @param $output OutputPage
  227. * @param $user User
  228. * @return int
  229. */
  230. private function showItemCount( $output, $user ) {
  231. if( ( $count = $this->countWatchlist( $user ) ) > 0 ) {
  232. $output->addHTML( wfMsgExt( 'watchlistedit-numitems', 'parse',
  233. $GLOBALS['wgLang']->formatNum( $count ) ) );
  234. } else {
  235. $output->addHTML( wfMsgExt( 'watchlistedit-noitems', 'parse' ) );
  236. }
  237. return $count;
  238. }
  239. /**
  240. * Remove all titles from a user's watchlist
  241. *
  242. * @param $user User
  243. */
  244. private function clearWatchlist( $user ) {
  245. $dbw = wfGetDB( DB_MASTER );
  246. $dbw->delete( 'watchlist', array( 'wl_user' => $user->getId() ), __METHOD__ );
  247. }
  248. /**
  249. * Add a list of titles to a user's watchlist
  250. *
  251. * $titles can be an array of strings or Title objects; the former
  252. * is preferred, since Titles are very memory-heavy
  253. *
  254. * @param $titles An array of strings, or Title objects
  255. * @param $user User
  256. */
  257. private function watchTitles( $titles, $user ) {
  258. $dbw = wfGetDB( DB_MASTER );
  259. $rows = array();
  260. foreach( $titles as $title ) {
  261. if( !$title instanceof Title )
  262. $title = Title::newFromText( $title );
  263. if( $title instanceof Title ) {
  264. $rows[] = array(
  265. 'wl_user' => $user->getId(),
  266. 'wl_namespace' => ( $title->getNamespace() & ~1 ),
  267. 'wl_title' => $title->getDBkey(),
  268. 'wl_notificationtimestamp' => null,
  269. );
  270. $rows[] = array(
  271. 'wl_user' => $user->getId(),
  272. 'wl_namespace' => ( $title->getNamespace() | 1 ),
  273. 'wl_title' => $title->getDBkey(),
  274. 'wl_notificationtimestamp' => null,
  275. );
  276. }
  277. }
  278. $dbw->insert( 'watchlist', $rows, __METHOD__, 'IGNORE' );
  279. }
  280. /**
  281. * Remove a list of titles from a user's watchlist
  282. *
  283. * $titles can be an array of strings or Title objects; the former
  284. * is preferred, since Titles are very memory-heavy
  285. *
  286. * @param $titles An array of strings, or Title objects
  287. * @param $user User
  288. */
  289. private function unwatchTitles( $titles, $user ) {
  290. $dbw = wfGetDB( DB_MASTER );
  291. foreach( $titles as $title ) {
  292. if( !$title instanceof Title )
  293. $title = Title::newFromText( $title );
  294. if( $title instanceof Title ) {
  295. $dbw->delete(
  296. 'watchlist',
  297. array(
  298. 'wl_user' => $user->getId(),
  299. 'wl_namespace' => ( $title->getNamespace() & ~1 ),
  300. 'wl_title' => $title->getDBkey(),
  301. ),
  302. __METHOD__
  303. );
  304. $dbw->delete(
  305. 'watchlist',
  306. array(
  307. 'wl_user' => $user->getId(),
  308. 'wl_namespace' => ( $title->getNamespace() | 1 ),
  309. 'wl_title' => $title->getDBkey(),
  310. ),
  311. __METHOD__
  312. );
  313. $article = new Article($title);
  314. wfRunHooks('UnwatchArticleComplete',array(&$user,&$article));
  315. }
  316. }
  317. }
  318. /**
  319. * Show the standard watchlist editing form
  320. *
  321. * @param $output OutputPage
  322. * @param $user User
  323. */
  324. private function showNormalForm( $output, $user ) {
  325. global $wgUser;
  326. if( ( $count = $this->showItemCount( $output, $user ) ) > 0 ) {
  327. $self = SpecialPage::getTitleFor( 'Watchlist' );
  328. $form = Xml::openElement( 'form', array( 'method' => 'post',
  329. 'action' => $self->getLocalUrl( 'action=edit' ) ) );
  330. $form .= Xml::hidden( 'token', $wgUser->editToken( 'watchlistedit' ) );
  331. $form .= "<fieldset>\n<legend>" . wfMsgHtml( 'watchlistedit-normal-legend' ) . "</legend>";
  332. $form .= wfMsgExt( 'watchlistedit-normal-explain', 'parse' );
  333. $form .= $this->buildRemoveList( $user, $wgUser->getSkin() );
  334. $form .= '<p>' . Xml::submitButton( wfMsg( 'watchlistedit-normal-submit' ) ) . '</p>';
  335. $form .= '</fieldset></form>';
  336. $output->addHTML( $form );
  337. }
  338. }
  339. /**
  340. * Build the part of the standard watchlist editing form with the actual
  341. * title selection checkboxes and stuff. Also generates a table of
  342. * contents if there's more than one heading.
  343. *
  344. * @param $user User
  345. * @param $skin Skin (really, Linker)
  346. */
  347. private function buildRemoveList( $user, $skin ) {
  348. $list = "";
  349. $toc = $skin->tocIndent();
  350. $tocLength = 0;
  351. foreach( $this->getWatchlistInfo( $user ) as $namespace => $pages ) {
  352. $tocLength++;
  353. $heading = htmlspecialchars( $this->getNamespaceHeading( $namespace ) );
  354. $anchor = "editwatchlist-ns" . $namespace;
  355. $list .= $skin->makeHeadLine( 2, ">", $anchor, $heading, "" );
  356. $toc .= $skin->tocLine( $anchor, $heading, $tocLength, 1 ) . $skin->tocLineEnd();
  357. $list .= "<ul>\n";
  358. foreach( $pages as $dbkey => $redirect ) {
  359. $title = Title::makeTitleSafe( $namespace, $dbkey );
  360. $list .= $this->buildRemoveLine( $title, $redirect, $skin );
  361. }
  362. $list .= "</ul>\n";
  363. }
  364. // ISSUE: omit the TOC if the total number of titles is low?
  365. if( $tocLength > 1 ) {
  366. $list = $skin->tocList( $toc ) . $list;
  367. }
  368. return $list;
  369. }
  370. /**
  371. * Get the correct "heading" for a namespace
  372. *
  373. * @param $namespace int
  374. * @return string
  375. */
  376. private function getNamespaceHeading( $namespace ) {
  377. return $namespace == NS_MAIN
  378. ? wfMsgHtml( 'blanknamespace' )
  379. : htmlspecialchars( $GLOBALS['wgContLang']->getFormattedNsText( $namespace ) );
  380. }
  381. /**
  382. * Build a single list item containing a check box selecting a title
  383. * and a link to that title, with various additional bits
  384. *
  385. * @param $title Title
  386. * @param $redirect bool
  387. * @param $skin Skin
  388. * @return string
  389. */
  390. private function buildRemoveLine( $title, $redirect, $skin ) {
  391. global $wgLang;
  392. $link = $skin->makeLinkObj( $title );
  393. if( $redirect )
  394. $link = '<span class="watchlistredir">' . $link . '</span>';
  395. $tools[] = $skin->makeLinkObj( $title->getTalkPage(), wfMsgHtml( 'talkpagelinktext' ) );
  396. if( $title->exists() ) {
  397. $tools[] = $skin->makeKnownLinkObj( $title, wfMsgHtml( 'history_short' ), 'action=history' );
  398. }
  399. if( $title->getNamespace() == NS_USER && !$title->isSubpage() ) {
  400. $tools[] = $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Contributions', $title->getText() ), wfMsgHtml( 'contributions' ) );
  401. }
  402. return "<li>"
  403. . Xml::check( 'titles[]', false, array( 'value' => $title->getPrefixedText() ) )
  404. . $link . " (" . $wgLang->pipeList( $tools ) . ")" . "</li>\n";
  405. }
  406. /**
  407. * Show a form for editing the watchlist in "raw" mode
  408. *
  409. * @param $output OutputPage
  410. * @param $user User
  411. */
  412. public function showRawForm( $output, $user ) {
  413. global $wgUser;
  414. $this->showItemCount( $output, $user );
  415. $self = SpecialPage::getTitleFor( 'Watchlist' );
  416. $form = Xml::openElement( 'form', array( 'method' => 'post',
  417. 'action' => $self->getLocalUrl( 'action=raw' ) ) );
  418. $form .= Xml::hidden( 'token', $wgUser->editToken( 'watchlistedit' ) );
  419. $form .= '<fieldset><legend>' . wfMsgHtml( 'watchlistedit-raw-legend' ) . '</legend>';
  420. $form .= wfMsgExt( 'watchlistedit-raw-explain', 'parse' );
  421. $form .= Xml::label( wfMsg( 'watchlistedit-raw-titles' ), 'titles' );
  422. $form .= "<br />\n";
  423. $form .= Xml::openElement( 'textarea', array( 'id' => 'titles', 'name' => 'titles',
  424. 'rows' => $wgUser->getIntOption( 'rows' ), 'cols' => $wgUser->getIntOption( 'cols' ) ) );
  425. $titles = $this->getWatchlist( $user );
  426. foreach( $titles as $title )
  427. $form .= htmlspecialchars( $title ) . "\n";
  428. $form .= '</textarea>';
  429. $form .= '<p>' . Xml::submitButton( wfMsg( 'watchlistedit-raw-submit' ) ) . '</p>';
  430. $form .= '</fieldset></form>';
  431. $output->addHTML( $form );
  432. }
  433. /**
  434. * Determine whether we are editing the watchlist, and if so, what
  435. * kind of editing operation
  436. *
  437. * @param $request WebRequest
  438. * @param $par mixed
  439. * @return int
  440. */
  441. public static function getMode( $request, $par ) {
  442. $mode = strtolower( $request->getVal( 'action', $par ) );
  443. switch( $mode ) {
  444. case 'clear':
  445. return self::EDIT_CLEAR;
  446. case 'raw':
  447. return self::EDIT_RAW;
  448. case 'edit':
  449. return self::EDIT_NORMAL;
  450. default:
  451. return false;
  452. }
  453. }
  454. /**
  455. * Build a set of links for convenient navigation
  456. * between watchlist viewing and editing modes
  457. *
  458. * @param $skin Skin to use
  459. * @return string
  460. */
  461. public static function buildTools( $skin ) {
  462. global $wgLang;
  463. $tools = array();
  464. $modes = array( 'view' => false, 'edit' => 'edit', 'raw' => 'raw' );
  465. foreach( $modes as $mode => $subpage ) {
  466. $tools[] = $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Watchlist', $subpage ), wfMsgHtml( "watchlisttools-{$mode}" ) );
  467. }
  468. return $wgLang->pipeList( $tools );
  469. }
  470. }