SpecialEditWatchlist.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792
  1. <?php
  2. /**
  3. * @defgroup Watchlist Users watchlist handling
  4. */
  5. /**
  6. * Implements Special:EditWatchlist
  7. *
  8. * This program is free software; you can redistribute it and/or modify
  9. * it under the terms of the GNU General Public License as published by
  10. * the Free Software Foundation; either version 2 of the License, or
  11. * (at your option) any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU General Public License along
  19. * with this program; if not, write to the Free Software Foundation, Inc.,
  20. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  21. * http://www.gnu.org/copyleft/gpl.html
  22. *
  23. * @file
  24. * @ingroup SpecialPage
  25. * @ingroup Watchlist
  26. */
  27. use MediaWiki\Linker\LinkRenderer;
  28. use MediaWiki\Linker\LinkTarget;
  29. use MediaWiki\MediaWikiServices;
  30. /**
  31. * Provides the UI through which users can perform editing
  32. * operations on their watchlist
  33. *
  34. * @ingroup SpecialPage
  35. * @ingroup Watchlist
  36. * @author Rob Church <robchur@gmail.com>
  37. */
  38. class SpecialEditWatchlist extends UnlistedSpecialPage {
  39. /**
  40. * Editing modes. EDIT_CLEAR is no longer used; the "Clear" link scared people
  41. * too much. Now it's passed on to the raw editor, from which it's very easy to clear.
  42. */
  43. const EDIT_CLEAR = 1;
  44. const EDIT_RAW = 2;
  45. const EDIT_NORMAL = 3;
  46. protected $successMessage;
  47. protected $toc;
  48. private $badItems = [];
  49. /**
  50. * @var TitleParser
  51. */
  52. private $titleParser;
  53. public function __construct() {
  54. parent::__construct( 'EditWatchlist', 'editmywatchlist' );
  55. }
  56. /**
  57. * Initialize any services we'll need (unless it has already been provided via a setter).
  58. * This allows for dependency injection even though we don't control object creation.
  59. */
  60. private function initServices() {
  61. if ( !$this->titleParser ) {
  62. $this->titleParser = MediaWikiServices::getInstance()->getTitleParser();
  63. }
  64. }
  65. public function doesWrites() {
  66. return true;
  67. }
  68. /**
  69. * Main execution point
  70. *
  71. * @param int $mode
  72. */
  73. public function execute( $mode ) {
  74. $this->initServices();
  75. $this->setHeaders();
  76. # Anons don't get a watchlist
  77. $this->requireLogin( 'watchlistanontext' );
  78. $out = $this->getOutput();
  79. $this->checkPermissions();
  80. $this->checkReadOnly();
  81. $this->outputHeader();
  82. $this->outputSubtitle();
  83. $out->addModuleStyles( 'mediawiki.special' );
  84. # B/C: $mode used to be waaay down the parameter list, and the first parameter
  85. # was $wgUser
  86. if ( $mode instanceof User ) {
  87. $args = func_get_args();
  88. if ( count( $args ) >= 4 ) {
  89. $mode = $args[3];
  90. }
  91. }
  92. $mode = self::getMode( $this->getRequest(), $mode );
  93. switch ( $mode ) {
  94. case self::EDIT_RAW:
  95. $out->setPageTitle( $this->msg( 'watchlistedit-raw-title' ) );
  96. $form = $this->getRawForm();
  97. if ( $form->show() ) {
  98. $out->addHTML( $this->successMessage );
  99. $out->addReturnTo( SpecialPage::getTitleFor( 'Watchlist' ) );
  100. }
  101. break;
  102. case self::EDIT_CLEAR:
  103. $out->setPageTitle( $this->msg( 'watchlistedit-clear-title' ) );
  104. $form = $this->getClearForm();
  105. if ( $form->show() ) {
  106. $out->addHTML( $this->successMessage );
  107. $out->addReturnTo( SpecialPage::getTitleFor( 'Watchlist' ) );
  108. }
  109. break;
  110. case self::EDIT_NORMAL:
  111. default:
  112. $this->executeViewEditWatchlist();
  113. break;
  114. }
  115. }
  116. /**
  117. * Renders a subheader on the watchlist page.
  118. */
  119. protected function outputSubtitle() {
  120. $out = $this->getOutput();
  121. $out->addSubtitle( $this->msg( 'watchlistfor2', $this->getUser()->getName() )
  122. ->rawParams(
  123. self::buildTools(
  124. $this->getLanguage(),
  125. $this->getLinkRenderer()
  126. )
  127. )
  128. );
  129. }
  130. /**
  131. * Executes an edit mode for the watchlist view, from which you can manage your watchlist
  132. */
  133. protected function executeViewEditWatchlist() {
  134. $out = $this->getOutput();
  135. $out->setPageTitle( $this->msg( 'watchlistedit-normal-title' ) );
  136. $form = $this->getNormalForm();
  137. if ( $form->show() ) {
  138. $out->addHTML( $this->successMessage );
  139. $out->addReturnTo( SpecialPage::getTitleFor( 'Watchlist' ) );
  140. } elseif ( $this->toc !== false ) {
  141. $out->prependHTML( $this->toc );
  142. $out->addModuleStyles( 'mediawiki.toc.styles' );
  143. }
  144. }
  145. /**
  146. * Return an array of subpages that this special page will accept.
  147. *
  148. * @see also SpecialWatchlist::getSubpagesForPrefixSearch
  149. * @return string[] subpages
  150. */
  151. public function getSubpagesForPrefixSearch() {
  152. // SpecialWatchlist uses SpecialEditWatchlist::getMode, so new types should be added
  153. // here and there - no 'edit' here, because that the default for this page
  154. return [
  155. 'clear',
  156. 'raw',
  157. ];
  158. }
  159. /**
  160. * Extract a list of titles from a blob of text, returning
  161. * (prefixed) strings; unwatchable titles are ignored
  162. *
  163. * @param string $list
  164. * @return array
  165. */
  166. private function extractTitles( $list ) {
  167. $list = explode( "\n", trim( $list ) );
  168. if ( !is_array( $list ) ) {
  169. return [];
  170. }
  171. $titles = [];
  172. foreach ( $list as $text ) {
  173. $text = trim( $text );
  174. if ( strlen( $text ) > 0 ) {
  175. $title = Title::newFromText( $text );
  176. if ( $title instanceof Title && $title->isWatchable() ) {
  177. $titles[] = $title;
  178. }
  179. }
  180. }
  181. MediaWikiServices::getInstance()->getGenderCache()->doTitlesArray( $titles );
  182. $list = [];
  183. /** @var Title $title */
  184. foreach ( $titles as $title ) {
  185. $list[] = $title->getPrefixedText();
  186. }
  187. return array_unique( $list );
  188. }
  189. public function submitRaw( $data ) {
  190. $wanted = $this->extractTitles( $data['Titles'] );
  191. $current = $this->getWatchlist();
  192. if ( count( $wanted ) > 0 ) {
  193. $toWatch = array_diff( $wanted, $current );
  194. $toUnwatch = array_diff( $current, $wanted );
  195. $this->watchTitles( $toWatch );
  196. $this->unwatchTitles( $toUnwatch );
  197. $this->getUser()->invalidateCache();
  198. if ( count( $toWatch ) > 0 || count( $toUnwatch ) > 0 ) {
  199. $this->successMessage = $this->msg( 'watchlistedit-raw-done' )->parse();
  200. } else {
  201. return false;
  202. }
  203. if ( count( $toWatch ) > 0 ) {
  204. $this->successMessage .= ' ' . $this->msg( 'watchlistedit-raw-added' )
  205. ->numParams( count( $toWatch ) )->parse();
  206. $this->showTitles( $toWatch, $this->successMessage );
  207. }
  208. if ( count( $toUnwatch ) > 0 ) {
  209. $this->successMessage .= ' ' . $this->msg( 'watchlistedit-raw-removed' )
  210. ->numParams( count( $toUnwatch ) )->parse();
  211. $this->showTitles( $toUnwatch, $this->successMessage );
  212. }
  213. } else {
  214. if ( count( $current ) === 0 ) {
  215. return false;
  216. }
  217. $this->clearUserWatchedItems( $current, 'raw' );
  218. $this->showTitles( $current, $this->successMessage );
  219. }
  220. return true;
  221. }
  222. public function submitClear( $data ) {
  223. $current = $this->getWatchlist();
  224. $this->clearUserWatchedItems( $current, 'clear' );
  225. $this->showTitles( $current, $this->successMessage );
  226. return true;
  227. }
  228. /**
  229. * @param array $current
  230. * @param string $messageFor 'raw' or 'clear'
  231. */
  232. private function clearUserWatchedItems( $current, $messageFor ) {
  233. $watchedItemStore = MediaWikiServices::getInstance()->getWatchedItemStore();
  234. if ( $watchedItemStore->clearUserWatchedItems( $this->getUser() ) ) {
  235. $this->successMessage = $this->msg( 'watchlistedit-' . $messageFor . '-done' )->parse();
  236. $this->successMessage .= ' ' . $this->msg( 'watchlistedit-' . $messageFor . '-removed' )
  237. ->numParams( count( $current ) )->parse();
  238. $this->getUser()->invalidateCache();
  239. } else {
  240. $watchedItemStore->clearUserWatchedItemsUsingJobQueue( $this->getUser() );
  241. $this->successMessage = $this->msg( 'watchlistedit-clear-jobqueue' )->parse();
  242. }
  243. }
  244. /**
  245. * Print out a list of linked titles
  246. *
  247. * $titles can be an array of strings or Title objects; the former
  248. * is preferred, since Titles are very memory-heavy
  249. *
  250. * @param array $titles Array of strings, or Title objects
  251. * @param string $output
  252. */
  253. private function showTitles( $titles, &$output ) {
  254. $talk = $this->msg( 'talkpagelinktext' )->text();
  255. // Do a batch existence check
  256. $batch = new LinkBatch();
  257. if ( count( $titles ) >= 100 ) {
  258. $output = $this->msg( 'watchlistedit-too-many' )->parse();
  259. return;
  260. }
  261. foreach ( $titles as $title ) {
  262. if ( !$title instanceof Title ) {
  263. $title = Title::newFromText( $title );
  264. }
  265. if ( $title instanceof Title ) {
  266. $batch->addObj( $title );
  267. $batch->addObj( $title->getTalkPage() );
  268. }
  269. }
  270. $batch->execute();
  271. // Print out the list
  272. $output .= "<ul>\n";
  273. $linkRenderer = $this->getLinkRenderer();
  274. foreach ( $titles as $title ) {
  275. if ( !$title instanceof Title ) {
  276. $title = Title::newFromText( $title );
  277. }
  278. if ( $title instanceof Title ) {
  279. $output .= '<li>' .
  280. $linkRenderer->makeLink( $title ) . ' ' .
  281. $this->msg( 'parentheses' )->rawParams(
  282. $linkRenderer->makeLink( $title->getTalkPage(), $talk )
  283. )->escaped() .
  284. "</li>\n";
  285. }
  286. }
  287. $output .= "</ul>\n";
  288. }
  289. /**
  290. * Prepare a list of titles on a user's watchlist (excluding talk pages)
  291. * and return an array of (prefixed) strings
  292. *
  293. * @return array
  294. */
  295. private function getWatchlist() {
  296. $list = [];
  297. $watchedItems = MediaWikiServices::getInstance()->getWatchedItemStore()->getWatchedItemsForUser(
  298. $this->getUser(),
  299. [ 'forWrite' => $this->getRequest()->wasPosted() ]
  300. );
  301. if ( $watchedItems ) {
  302. /** @var Title[] $titles */
  303. $titles = [];
  304. foreach ( $watchedItems as $watchedItem ) {
  305. $namespace = $watchedItem->getLinkTarget()->getNamespace();
  306. $dbKey = $watchedItem->getLinkTarget()->getDBkey();
  307. $title = Title::makeTitleSafe( $namespace, $dbKey );
  308. if ( $this->checkTitle( $title, $namespace, $dbKey )
  309. && !$title->isTalkPage()
  310. ) {
  311. $titles[] = $title;
  312. }
  313. }
  314. MediaWikiServices::getInstance()->getGenderCache()->doTitlesArray( $titles );
  315. foreach ( $titles as $title ) {
  316. $list[] = $title->getPrefixedText();
  317. }
  318. }
  319. $this->cleanupWatchlist();
  320. return $list;
  321. }
  322. /**
  323. * Get a list of titles on a user's watchlist, excluding talk pages,
  324. * and return as a two-dimensional array with namespace and title.
  325. *
  326. * @return array
  327. */
  328. protected function getWatchlistInfo() {
  329. $titles = [];
  330. $services = MediaWikiServices::getInstance();
  331. $watchedItems = $services->getWatchedItemStore()
  332. ->getWatchedItemsForUser( $this->getUser(), [ 'sort' => WatchedItemStore::SORT_ASC ] );
  333. $lb = new LinkBatch();
  334. foreach ( $watchedItems as $watchedItem ) {
  335. $namespace = $watchedItem->getLinkTarget()->getNamespace();
  336. $dbKey = $watchedItem->getLinkTarget()->getDBkey();
  337. $lb->add( $namespace, $dbKey );
  338. if ( !$services->getNamespaceInfo()->isTalk( $namespace ) ) {
  339. $titles[$namespace][$dbKey] = 1;
  340. }
  341. }
  342. $lb->execute();
  343. return $titles;
  344. }
  345. /**
  346. * Validates watchlist entry
  347. *
  348. * @param Title $title
  349. * @param int $namespace
  350. * @param string $dbKey
  351. * @return bool Whether this item is valid
  352. */
  353. private function checkTitle( $title, $namespace, $dbKey ) {
  354. if ( $title
  355. && ( $title->isExternal()
  356. || $title->getNamespace() < 0
  357. )
  358. ) {
  359. $title = false; // unrecoverable
  360. }
  361. if ( !$title
  362. || $title->getNamespace() != $namespace
  363. || $title->getDBkey() != $dbKey
  364. ) {
  365. $this->badItems[] = [ $title, $namespace, $dbKey ];
  366. }
  367. return (bool)$title;
  368. }
  369. /**
  370. * Attempts to clean up broken items
  371. */
  372. private function cleanupWatchlist() {
  373. if ( $this->badItems === [] ) {
  374. return; // nothing to do
  375. }
  376. $user = $this->getUser();
  377. $badItems = $this->badItems;
  378. DeferredUpdates::addCallableUpdate( function () use ( $user, $badItems ) {
  379. $store = MediaWikiServices::getInstance()->getWatchedItemStore();
  380. foreach ( $badItems as $row ) {
  381. list( $title, $namespace, $dbKey ) = $row;
  382. $action = $title ? 'cleaning up' : 'deleting';
  383. wfDebug( "User {$user->getName()} has broken watchlist item " .
  384. "ns($namespace):$dbKey, $action.\n" );
  385. $store->removeWatch( $user, new TitleValue( (int)$namespace, $dbKey ) );
  386. // Can't just do an UPDATE instead of DELETE/INSERT due to unique index
  387. if ( $title ) {
  388. $user->addWatch( $title );
  389. }
  390. }
  391. } );
  392. }
  393. /**
  394. * Add a list of targets to a user's watchlist
  395. *
  396. * @param string[]|LinkTarget[] $targets
  397. * @return bool
  398. * @throws FatalError
  399. * @throws MWException
  400. */
  401. private function watchTitles( array $targets ) {
  402. return MediaWikiServices::getInstance()->getWatchedItemStore()
  403. ->addWatchBatchForUser( $this->getUser(), $this->getExpandedTargets( $targets ) )
  404. && $this->runWatchUnwatchCompleteHook( 'Watch', $targets );
  405. }
  406. /**
  407. * Remove a list of titles from a user's watchlist
  408. *
  409. * $titles can be an array of strings or Title objects; the former
  410. * is preferred, since Titles are very memory-heavy
  411. *
  412. * @param string[]|LinkTarget[] $targets
  413. *
  414. * @return bool
  415. * @throws FatalError
  416. * @throws MWException
  417. */
  418. private function unwatchTitles( array $targets ) {
  419. return MediaWikiServices::getInstance()->getWatchedItemStore()
  420. ->removeWatchBatchForUser( $this->getUser(), $this->getExpandedTargets( $targets ) )
  421. && $this->runWatchUnwatchCompleteHook( 'Unwatch', $targets );
  422. }
  423. /**
  424. * @param string $action
  425. * Can be "Watch" or "Unwatch"
  426. * @param string[]|LinkTarget[] $targets
  427. * @return bool
  428. * @throws FatalError
  429. * @throws MWException
  430. */
  431. private function runWatchUnwatchCompleteHook( $action, $targets ) {
  432. foreach ( $targets as $target ) {
  433. $title = $target instanceof TitleValue ?
  434. Title::newFromTitleValue( $target ) :
  435. Title::newFromText( $target );
  436. $page = WikiPage::factory( $title );
  437. Hooks::run( $action . 'ArticleComplete', [ $this->getUser(), &$page ] );
  438. }
  439. return true;
  440. }
  441. /**
  442. * @param string[]|LinkTarget[] $targets
  443. * @return TitleValue[]
  444. */
  445. private function getExpandedTargets( array $targets ) {
  446. $expandedTargets = [];
  447. $services = MediaWikiServices::getInstance();
  448. foreach ( $targets as $target ) {
  449. if ( !$target instanceof LinkTarget ) {
  450. try {
  451. $target = $this->titleParser->parseTitle( $target, NS_MAIN );
  452. }
  453. catch ( MalformedTitleException $e ) {
  454. continue;
  455. }
  456. }
  457. $ns = $target->getNamespace();
  458. $dbKey = $target->getDBkey();
  459. $expandedTargets[] =
  460. new TitleValue( $services->getNamespaceInfo()->getSubject( $ns ), $dbKey );
  461. $expandedTargets[] =
  462. new TitleValue( $services->getNamespaceInfo()->getTalk( $ns ), $dbKey );
  463. }
  464. return $expandedTargets;
  465. }
  466. public function submitNormal( $data ) {
  467. $removed = [];
  468. foreach ( $data as $titles ) {
  469. $this->unwatchTitles( $titles );
  470. $removed = array_merge( $removed, $titles );
  471. }
  472. if ( count( $removed ) > 0 ) {
  473. $this->successMessage = $this->msg( 'watchlistedit-normal-done'
  474. )->numParams( count( $removed ) )->parse();
  475. $this->showTitles( $removed, $this->successMessage );
  476. return true;
  477. } else {
  478. return false;
  479. }
  480. }
  481. /**
  482. * Get the standard watchlist editing form
  483. *
  484. * @return HTMLForm
  485. */
  486. protected function getNormalForm() {
  487. $fields = [];
  488. $count = 0;
  489. // Allow subscribers to manipulate the list of watched pages (or use it
  490. // to preload lots of details at once)
  491. $watchlistInfo = $this->getWatchlistInfo();
  492. Hooks::run(
  493. 'WatchlistEditorBeforeFormRender',
  494. [ &$watchlistInfo ]
  495. );
  496. foreach ( $watchlistInfo as $namespace => $pages ) {
  497. $options = [];
  498. foreach ( array_keys( $pages ) as $dbkey ) {
  499. $title = Title::makeTitleSafe( $namespace, $dbkey );
  500. if ( $this->checkTitle( $title, $namespace, $dbkey ) ) {
  501. $text = $this->buildRemoveLine( $title );
  502. $options[$text] = $title->getPrefixedText();
  503. $count++;
  504. }
  505. }
  506. // checkTitle can filter some options out, avoid empty sections
  507. if ( count( $options ) > 0 ) {
  508. $fields['TitlesNs' . $namespace] = [
  509. 'class' => EditWatchlistCheckboxSeriesField::class,
  510. 'options' => $options,
  511. 'section' => "ns$namespace",
  512. ];
  513. }
  514. }
  515. $this->cleanupWatchlist();
  516. if ( count( $fields ) > 1 && $count > 30 ) {
  517. $this->toc = Linker::tocIndent();
  518. $tocLength = 0;
  519. $contLang = MediaWikiServices::getInstance()->getContentLanguage();
  520. foreach ( $fields as $data ) {
  521. # strip out the 'ns' prefix from the section name:
  522. $ns = substr( $data['section'], 2 );
  523. $nsText = ( $ns == NS_MAIN )
  524. ? $this->msg( 'blanknamespace' )->escaped()
  525. : htmlspecialchars( $contLang->getFormattedNsText( $ns ) );
  526. $this->toc .= Linker::tocLine( "editwatchlist-{$data['section']}", $nsText,
  527. $this->getLanguage()->formatNum( ++$tocLength ), 1 ) . Linker::tocLineEnd();
  528. }
  529. $this->toc = Linker::tocList( $this->toc );
  530. } else {
  531. $this->toc = false;
  532. }
  533. $context = new DerivativeContext( $this->getContext() );
  534. $context->setTitle( $this->getPageTitle() ); // Remove subpage
  535. $form = new EditWatchlistNormalHTMLForm( $fields, $context );
  536. $form->setSubmitTextMsg( 'watchlistedit-normal-submit' );
  537. $form->setSubmitDestructive();
  538. # Used message keys:
  539. # 'accesskey-watchlistedit-normal-submit', 'tooltip-watchlistedit-normal-submit'
  540. $form->setSubmitTooltip( 'watchlistedit-normal-submit' );
  541. $form->setWrapperLegendMsg( 'watchlistedit-normal-legend' );
  542. $form->addHeaderText( $this->msg( 'watchlistedit-normal-explain' )->parse() );
  543. $form->setSubmitCallback( [ $this, 'submitNormal' ] );
  544. return $form;
  545. }
  546. /**
  547. * Build the label for a checkbox, with a link to the title, and various additional bits
  548. *
  549. * @param Title $title
  550. * @return string
  551. */
  552. private function buildRemoveLine( $title ) {
  553. $linkRenderer = $this->getLinkRenderer();
  554. $link = $linkRenderer->makeLink( $title );
  555. $tools = [];
  556. $tools['talk'] = $linkRenderer->makeLink(
  557. $title->getTalkPage(),
  558. $this->msg( 'talkpagelinktext' )->text()
  559. );
  560. if ( $title->exists() ) {
  561. $tools['history'] = $linkRenderer->makeKnownLink(
  562. $title,
  563. $this->msg( 'history_small' )->text(),
  564. [],
  565. [ 'action' => 'history' ]
  566. );
  567. }
  568. if ( $title->getNamespace() == NS_USER && !$title->isSubpage() ) {
  569. $tools['contributions'] = $linkRenderer->makeKnownLink(
  570. SpecialPage::getTitleFor( 'Contributions', $title->getText() ),
  571. $this->msg( 'contribslink' )->text()
  572. );
  573. }
  574. Hooks::run(
  575. 'WatchlistEditorBuildRemoveLine',
  576. [ &$tools, $title, $title->isRedirect(), $this->getSkin(), &$link ]
  577. );
  578. if ( $title->isRedirect() ) {
  579. // Linker already makes class mw-redirect, so this is redundant
  580. $link = '<span class="watchlistredir">' . $link . '</span>';
  581. }
  582. return $link . ' ' .
  583. $this->msg( 'parentheses' )->rawParams( $this->getLanguage()->pipeList( $tools ) )->escaped();
  584. }
  585. /**
  586. * Get a form for editing the watchlist in "raw" mode
  587. *
  588. * @return HTMLForm
  589. */
  590. protected function getRawForm() {
  591. $titles = implode( "\n", $this->getWatchlist() );
  592. $fields = [
  593. 'Titles' => [
  594. 'type' => 'textarea',
  595. 'label-message' => 'watchlistedit-raw-titles',
  596. 'default' => $titles,
  597. ],
  598. ];
  599. $context = new DerivativeContext( $this->getContext() );
  600. $context->setTitle( $this->getPageTitle( 'raw' ) ); // Reset subpage
  601. $form = new OOUIHTMLForm( $fields, $context );
  602. $form->setSubmitTextMsg( 'watchlistedit-raw-submit' );
  603. # Used message keys: 'accesskey-watchlistedit-raw-submit', 'tooltip-watchlistedit-raw-submit'
  604. $form->setSubmitTooltip( 'watchlistedit-raw-submit' );
  605. $form->setWrapperLegendMsg( 'watchlistedit-raw-legend' );
  606. $form->addHeaderText( $this->msg( 'watchlistedit-raw-explain' )->parse() );
  607. $form->setSubmitCallback( [ $this, 'submitRaw' ] );
  608. return $form;
  609. }
  610. /**
  611. * Get a form for clearing the watchlist
  612. *
  613. * @return HTMLForm
  614. */
  615. protected function getClearForm() {
  616. $context = new DerivativeContext( $this->getContext() );
  617. $context->setTitle( $this->getPageTitle( 'clear' ) ); // Reset subpage
  618. $form = new OOUIHTMLForm( [], $context );
  619. $form->setSubmitTextMsg( 'watchlistedit-clear-submit' );
  620. # Used message keys: 'accesskey-watchlistedit-clear-submit', 'tooltip-watchlistedit-clear-submit'
  621. $form->setSubmitTooltip( 'watchlistedit-clear-submit' );
  622. $form->setWrapperLegendMsg( 'watchlistedit-clear-legend' );
  623. $form->addHeaderText( $this->msg( 'watchlistedit-clear-explain' )->parse() );
  624. $form->setSubmitCallback( [ $this, 'submitClear' ] );
  625. $form->setSubmitDestructive();
  626. return $form;
  627. }
  628. /**
  629. * Determine whether we are editing the watchlist, and if so, what
  630. * kind of editing operation
  631. *
  632. * @param WebRequest $request
  633. * @param string $par
  634. * @return int
  635. */
  636. public static function getMode( $request, $par ) {
  637. $mode = strtolower( $request->getVal( 'action', $par ) );
  638. switch ( $mode ) {
  639. case 'clear':
  640. case self::EDIT_CLEAR:
  641. return self::EDIT_CLEAR;
  642. case 'raw':
  643. case self::EDIT_RAW:
  644. return self::EDIT_RAW;
  645. case 'edit':
  646. case self::EDIT_NORMAL:
  647. return self::EDIT_NORMAL;
  648. default:
  649. return false;
  650. }
  651. }
  652. /**
  653. * Build a set of links for convenient navigation
  654. * between watchlist viewing and editing modes
  655. *
  656. * @param Language $lang
  657. * @param LinkRenderer|null $linkRenderer
  658. * @return string
  659. */
  660. public static function buildTools( $lang, LinkRenderer $linkRenderer = null ) {
  661. if ( !$lang instanceof Language ) {
  662. // back-compat where the first parameter was $unused
  663. global $wgLang;
  664. $lang = $wgLang;
  665. }
  666. if ( !$linkRenderer ) {
  667. $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
  668. }
  669. $tools = [];
  670. $modes = [
  671. 'view' => [ 'Watchlist', false ],
  672. 'edit' => [ 'EditWatchlist', false ],
  673. 'raw' => [ 'EditWatchlist', 'raw' ],
  674. 'clear' => [ 'EditWatchlist', 'clear' ],
  675. ];
  676. foreach ( $modes as $mode => $arr ) {
  677. // can use messages 'watchlisttools-view', 'watchlisttools-edit', 'watchlisttools-raw'
  678. $tools[] = $linkRenderer->makeKnownLink(
  679. SpecialPage::getTitleFor( $arr[0], $arr[1] ),
  680. wfMessage( "watchlisttools-{$mode}" )->text()
  681. );
  682. }
  683. return Html::rawElement(
  684. 'span',
  685. [ 'class' => 'mw-watchlist-toollinks' ],
  686. wfMessage( 'parentheses' )->rawParams( $lang->pipeList( $tools ) )->escaped()
  687. );
  688. }
  689. }