SpecialContributions.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565
  1. <?php
  2. /**
  3. * Special:Contributions, show user contributions in a paged list
  4. * @file
  5. * @ingroup SpecialPage
  6. */
  7. class SpecialContributions extends SpecialPage {
  8. public function __construct() {
  9. parent::__construct( 'Contributions' );
  10. }
  11. public function execute( $par ) {
  12. global $wgUser, $wgOut, $wgLang, $wgRequest;
  13. $this->setHeaders();
  14. $this->outputHeader();
  15. $this->opts = array();
  16. if( $par == 'newbies' ) {
  17. $target = 'newbies';
  18. $this->opts['contribs'] = 'newbie';
  19. } elseif( isset( $par ) ) {
  20. $target = $par;
  21. } else {
  22. $target = $wgRequest->getVal( 'target' );
  23. }
  24. // check for radiobox
  25. if( $wgRequest->getVal( 'contribs' ) == 'newbie' ) {
  26. $target = 'newbies';
  27. $this->opts['contribs'] = 'newbie';
  28. }
  29. if( !strlen( $target ) ) {
  30. $wgOut->addHTML( $this->getForm() );
  31. return;
  32. }
  33. $this->opts['limit'] = $wgRequest->getInt( 'limit', 50 );
  34. $this->opts['target'] = $target;
  35. $nt = Title::makeTitleSafe( NS_USER, $target );
  36. if( !$nt ) {
  37. $wgOut->addHTML( $this->getForm() );
  38. return;
  39. }
  40. $id = User::idFromName( $nt->getText() );
  41. if( $target != 'newbies' ) {
  42. $target = $nt->getText();
  43. $wgOut->setSubtitle( $this->contributionsSub( $nt, $id ) );
  44. $wgOut->setHTMLTitle( wfMsg( 'pagetitle', wfMsgExt( 'contributions-title', array( 'parsemag' ),$target ) ) );
  45. } else {
  46. $wgOut->setSubtitle( wfMsgHtml( 'sp-contributions-newbies-sub') );
  47. $wgOut->setHTMLTitle( wfMsg( 'pagetitle', wfMsg( 'sp-contributions-newbies-title' ) ) );
  48. }
  49. if( ( $ns = $wgRequest->getVal( 'namespace', null ) ) !== null && $ns !== '' ) {
  50. $this->opts['namespace'] = intval( $ns );
  51. } else {
  52. $this->opts['namespace'] = '';
  53. }
  54. $this->opts['tagfilter'] = (string) $wgRequest->getVal( 'tagfilter' );
  55. // Allows reverts to have the bot flag in recent changes. It is just here to
  56. // be passed in the form at the top of the page
  57. if( $wgUser->isAllowed( 'markbotedits' ) && $wgRequest->getBool( 'bot' ) ) {
  58. $this->opts['bot'] = '1';
  59. }
  60. $skip = $wgRequest->getText( 'offset' ) || $wgRequest->getText( 'dir' ) == 'prev';
  61. # Offset overrides year/month selection
  62. if( $skip ) {
  63. $this->opts['year'] = '';
  64. $this->opts['month'] = '';
  65. } else {
  66. $this->opts['year'] = $wgRequest->getIntOrNull( 'year' );
  67. $this->opts['month'] = $wgRequest->getIntOrNull( 'month' );
  68. }
  69. // Add RSS/atom links
  70. $this->setSyndicated();
  71. $feedType = $wgRequest->getVal( 'feed' );
  72. if( $feedType ) {
  73. return $this->feed( $feedType );
  74. }
  75. wfRunHooks( 'SpecialContributionsBeforeMainOutput', $id );
  76. $wgOut->addHTML( $this->getForm() );
  77. $pager = new ContribsPager( $target, $this->opts['namespace'], $this->opts['year'], $this->opts['month'] );
  78. if( !$pager->getNumRows() ) {
  79. $wgOut->addWikiMsg( 'nocontribs', $target );
  80. return;
  81. }
  82. # Show a message about slave lag, if applicable
  83. if( ( $lag = $pager->getDatabase()->getLag() ) > 0 )
  84. $wgOut->showLagWarning( $lag );
  85. $wgOut->addHTML(
  86. '<p>' . $pager->getNavigationBar() . '</p>' .
  87. $pager->getBody() .
  88. '<p>' . $pager->getNavigationBar() . '</p>'
  89. );
  90. # If there were contributions, and it was a valid user or IP, show
  91. # the appropriate "footer" message - WHOIS tools, etc.
  92. if( $target != 'newbies' ) {
  93. $message = IP::isIPAddress( $target ) ?
  94. 'sp-contributions-footer-anon' : 'sp-contributions-footer';
  95. $text = wfMsgNoTrans( $message, $target );
  96. if( !wfEmptyMsg( $message, $text ) && $text != '-' ) {
  97. $wgOut->addHTML( '<div class="mw-contributions-footer">' );
  98. $wgOut->addWikiText( $text );
  99. $wgOut->addHTML( '</div>' );
  100. }
  101. }
  102. }
  103. protected function setSyndicated() {
  104. global $wgOut;
  105. $queryParams = array(
  106. 'namespace' => $this->opts['namespace'],
  107. 'target' => $this->opts['target']
  108. );
  109. $wgOut->setSyndicated( true );
  110. $wgOut->setFeedAppendQuery( wfArrayToCGI( $queryParams ) );
  111. }
  112. /**
  113. * Generates the subheading with links
  114. * @param Title $nt Title object for the target
  115. * @param integer $id User ID for the target
  116. * @return String: appropriately-escaped HTML to be output literally
  117. */
  118. protected function contributionsSub( $nt, $id ) {
  119. global $wgSysopUserBans, $wgLang, $wgUser;
  120. $sk = $wgUser->getSkin();
  121. if( 0 == $id ) {
  122. $user = $nt->getText();
  123. } else {
  124. $user = $sk->makeLinkObj( $nt, htmlspecialchars( $nt->getText() ) );
  125. }
  126. $talk = $nt->getTalkPage();
  127. if( $talk ) {
  128. # Talk page link
  129. $tools[] = $sk->makeLinkObj( $talk, wfMsgHtml( 'talkpagelinktext' ) );
  130. if( ( $id != 0 && $wgSysopUserBans ) || ( $id == 0 && IP::isIPAddress( $nt->getText() ) ) ) {
  131. # Block link
  132. if( $wgUser->isAllowed( 'block' ) )
  133. $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Blockip',
  134. $nt->getDBkey() ), wfMsgHtml( 'blocklink' ) );
  135. # Block log link
  136. $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ),
  137. wfMsgHtml( 'sp-contributions-blocklog' ), 'type=block&page=' . $nt->getPrefixedUrl() );
  138. }
  139. # Other logs link
  140. $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), wfMsg( 'sp-contributions-logs' ),
  141. 'user=' . $nt->getPartialUrl() );
  142. # Add link to deleted user contributions for priviledged users
  143. if( $wgUser->isAllowed( 'deletedhistory' ) ) {
  144. $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'DeletedContributions',
  145. $nt->getDBkey() ), wfMsgHtml( 'deletedcontributions' ) );
  146. }
  147. # Add a link to change user rights for privileged users
  148. $userrightsPage = new UserrightsPage();
  149. if( 0 !== $id && $userrightsPage->userCanChangeRights( User::newFromId( $id ) ) ) {
  150. $tools[] = $sk->makeKnownLinkObj(
  151. SpecialPage::getTitleFor( 'Userrights', $nt->getDBkey() ),
  152. wfMsgHtml( 'userrights' )
  153. );
  154. }
  155. wfRunHooks( 'ContributionsToolLinks', array( $id, $nt, &$tools ) );
  156. $links = $wgLang->pipeList( $tools );
  157. }
  158. // Old message 'contribsub' had one parameter, but that doesn't work for
  159. // languages that want to put the "for" bit right after $user but before
  160. // $links. If 'contribsub' is around, use it for reverse compatibility,
  161. // otherwise use 'contribsub2'.
  162. if( wfEmptyMsg( 'contribsub', wfMsg( 'contribsub' ) ) ) {
  163. return wfMsgHtml( 'contribsub2', $user, $links );
  164. } else {
  165. return wfMsgHtml( 'contribsub', "$user ($links)" );
  166. }
  167. }
  168. /**
  169. * Generates the namespace selector form with hidden attributes.
  170. * @param $this->opts Array: the options to be included.
  171. */
  172. protected function getForm() {
  173. global $wgScript, $wgTitle;
  174. $this->opts['title'] = $wgTitle->getPrefixedText();
  175. if( !isset( $this->opts['target'] ) ) {
  176. $this->opts['target'] = '';
  177. } else {
  178. $this->opts['target'] = str_replace( '_' , ' ' , $this->opts['target'] );
  179. }
  180. if( !isset( $this->opts['namespace'] ) ) {
  181. $this->opts['namespace'] = '';
  182. }
  183. if( !isset( $this->opts['contribs'] ) ) {
  184. $this->opts['contribs'] = 'user';
  185. }
  186. if( !isset( $this->opts['year'] ) ) {
  187. $this->opts['year'] = '';
  188. }
  189. if( !isset( $this->opts['month'] ) ) {
  190. $this->opts['month'] = '';
  191. }
  192. if( $this->opts['contribs'] == 'newbie' ) {
  193. $this->opts['target'] = '';
  194. }
  195. if( !isset( $this->opts['tagfilter'] ) ) {
  196. $this->opts['tagfilter'] = '';
  197. }
  198. $f = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) );
  199. # Add hidden params for tracking
  200. foreach ( $this->opts as $name => $value ) {
  201. if( in_array( $name, array( 'namespace', 'target', 'contribs', 'year', 'month' ) ) ) {
  202. continue;
  203. }
  204. $f .= "\t" . Xml::hidden( $name, $value ) . "\n";
  205. }
  206. $tagFilter = ChangeTags::buildTagFilterSelector( $this->opts['tagfilter'] );
  207. $f .= '<fieldset>' .
  208. Xml::element( 'legend', array(), wfMsg( 'sp-contributions-search' ) ) .
  209. Xml::radioLabel( wfMsgExt( 'sp-contributions-newbies', array( 'parseinline' ) ),
  210. 'contribs', 'newbie' , 'newbie', $this->opts['contribs'] == 'newbie' ? true : false ) . '<br />' .
  211. Xml::radioLabel( wfMsgExt( 'sp-contributions-username', array( 'parseinline' ) ),
  212. 'contribs' , 'user', 'user', $this->opts['contribs'] == 'user' ? true : false ) . ' ' .
  213. Xml::input( 'target', 20, $this->opts['target']) . ' '.
  214. '<span style="white-space: nowrap">' .
  215. Xml::label( wfMsg( 'namespace' ), 'namespace' ) . ' ' .
  216. Xml::namespaceSelector( $this->opts['namespace'], '' ) .
  217. '</span>' .
  218. ( $tagFilter ? Xml::tags( 'p', null, implode( '&nbsp;', $tagFilter ) ) : '' ) .
  219. Xml::openElement( 'p' ) .
  220. '<span style="white-space: nowrap">' .
  221. Xml::dateMenu( $this->opts['year'], $this->opts['month'] ) .
  222. '</span>' . ' ' .
  223. Xml::submitButton( wfMsg( 'sp-contributions-submit' ) ) .
  224. Xml::closeElement( 'p' );
  225. $explain = wfMsgExt( 'sp-contributions-explain', 'parseinline' );
  226. if( !wfEmptyMsg( 'sp-contributions-explain', $explain ) )
  227. $f .= "<p>{$explain}</p>";
  228. $f .= '</fieldset>' .
  229. Xml::closeElement( 'form' );
  230. return $f;
  231. }
  232. /**
  233. * Output a subscription feed listing recent edits to this page.
  234. * @param string $type
  235. */
  236. protected function feed( $type ) {
  237. global $wgRequest, $wgFeed, $wgFeedClasses, $wgFeedLimit;
  238. if( !$wgFeed ) {
  239. global $wgOut;
  240. $wgOut->addWikiMsg( 'feed-unavailable' );
  241. return;
  242. }
  243. if( !isset( $wgFeedClasses[$type] ) ) {
  244. global $wgOut;
  245. $wgOut->addWikiMsg( 'feed-invalid' );
  246. return;
  247. }
  248. $feed = new $wgFeedClasses[$type](
  249. $this->feedTitle(),
  250. wfMsgExt( 'tagline', 'parsemag' ),
  251. $this->getTitle()->getFullUrl() . "/" . urlencode($this->opts['target'])
  252. );
  253. // Already valid title
  254. $nt = Title::makeTitleSafe( NS_USER, $this->opts['target'] );
  255. $target = $this->opts['target'] == 'newbies' ? 'newbies' : $nt->getText();
  256. $pager = new ContribsPager( $target, $this->opts['namespace'],
  257. $this->opts['year'], $this->opts['month'], $this->opts['tagfilter'] );
  258. $pager->mLimit = min( $this->opts['limit'], $wgFeedLimit );
  259. $feed->outHeader();
  260. if( $pager->getNumRows() > 0 ) {
  261. while( $row = $pager->mResult->fetchObject() ) {
  262. $feed->outItem( $this->feedItem( $row ) );
  263. }
  264. }
  265. $feed->outFooter();
  266. }
  267. protected function feedTitle() {
  268. global $wgContLanguageCode, $wgSitename;
  269. $page = SpecialPage::getPage( 'Contributions' );
  270. $desc = $page->getDescription();
  271. return "$wgSitename - $desc [$wgContLanguageCode]";
  272. }
  273. protected function feedItem( $row ) {
  274. $title = Title::MakeTitle( intval( $row->page_namespace ), $row->page_title );
  275. if( $title ) {
  276. $date = $row->rev_timestamp;
  277. $comments = $title->getTalkPage()->getFullURL();
  278. $revision = Revision::newFromTitle( $title, $row->rev_id );
  279. return new FeedItem(
  280. $title->getPrefixedText(),
  281. $this->feedItemDesc( $revision ),
  282. $title->getFullURL(),
  283. $date,
  284. $this->feedItemAuthor( $revision ),
  285. $comments
  286. );
  287. } else {
  288. return NULL;
  289. }
  290. }
  291. protected function feedItemAuthor( $revision ) {
  292. return $revision->getUserText();
  293. }
  294. protected function feedItemDesc( $revision ) {
  295. if( $revision ) {
  296. return '<p>' . htmlspecialchars( $revision->getUserText() ) . wfMsgForContent( 'colon-separator' ) .
  297. htmlspecialchars( FeedItem::stripComment( $revision->getComment() ) ) .
  298. "</p>\n<hr />\n<div>" .
  299. nl2br( htmlspecialchars( $revision->getText() ) ) . "</div>";
  300. }
  301. return '';
  302. }
  303. }
  304. /**
  305. * Pager for Special:Contributions
  306. * @ingroup SpecialPage Pager
  307. */
  308. class ContribsPager extends ReverseChronologicalPager {
  309. public $mDefaultDirection = true;
  310. var $messages, $target;
  311. var $namespace = '', $mDb;
  312. function __construct( $target, $namespace = false, $year = false, $month = false, $tagFilter = false ) {
  313. parent::__construct();
  314. foreach( explode( ' ', 'uctop diff newarticle rollbacklink diff hist newpageletter minoreditletter' ) as $msg ) {
  315. $this->messages[$msg] = wfMsgExt( $msg, array( 'escape') );
  316. }
  317. $this->target = $target;
  318. $this->namespace = $namespace;
  319. $this->tagFilter = $tagFilter;
  320. $this->getDateCond( $year, $month );
  321. $this->mDb = wfGetDB( DB_SLAVE, 'contributions' );
  322. }
  323. function getDefaultQuery() {
  324. $query = parent::getDefaultQuery();
  325. $query['target'] = $this->target;
  326. return $query;
  327. }
  328. function getQueryInfo() {
  329. global $wgUser;
  330. list( $tables, $index, $userCond, $join_cond ) = $this->getUserCond();
  331. $conds = array_merge( $userCond, $this->getNamespaceCond() );
  332. // Paranoia: avoid brute force searches (bug 17342)
  333. if( !$wgUser->isAllowed( 'suppressrevision' ) ) {
  334. $conds[] = 'rev_deleted & ' . Revision::DELETED_USER . ' = 0';
  335. }
  336. $join_cond['page'] = array( 'INNER JOIN', 'page_id=rev_page' );
  337. $queryInfo = array(
  338. 'tables' => $tables,
  339. 'fields' => array(
  340. 'page_namespace', 'page_title', 'page_is_new', 'page_latest', 'page_is_redirect',
  341. 'page_len','rev_id', 'rev_page', 'rev_text_id', 'rev_timestamp', 'rev_comment',
  342. 'rev_minor_edit', 'rev_user', 'rev_user_text', 'rev_parent_id', 'rev_deleted'
  343. ),
  344. 'conds' => $conds,
  345. 'options' => array( 'USE INDEX' => array('revision' => $index) ),
  346. 'join_conds' => $join_cond
  347. );
  348. ChangeTags::modifyDisplayQuery( $queryInfo['tables'],
  349. $queryInfo['fields'],
  350. $queryInfo['conds'],
  351. $queryInfo['join_conds'],
  352. $queryInfo['options'],
  353. $this->tagFilter );
  354. wfRunHooks( 'ContribsPager::getQueryInfo', array( &$this, &$queryInfo ) );
  355. return $queryInfo;
  356. }
  357. function getUserCond() {
  358. $condition = array();
  359. $join_conds = array();
  360. if( $this->target == 'newbies' ) {
  361. $tables = array( 'user_groups', 'page', 'revision' );
  362. $max = $this->mDb->selectField( 'user', 'max(user_id)', false, __METHOD__ );
  363. $condition[] = 'rev_user >' . (int)($max - $max / 100);
  364. $condition[] = 'ug_group IS NULL';
  365. $index = 'user_timestamp';
  366. # FIXME: other groups may have 'bot' rights
  367. $join_conds['user_groups'] = array( 'LEFT JOIN', "ug_user = rev_user AND ug_group = 'bot'" );
  368. } else {
  369. $tables = array( 'page', 'revision' );
  370. $condition['rev_user_text'] = $this->target;
  371. $index = 'usertext_timestamp';
  372. }
  373. return array( $tables, $index, $condition, $join_conds );
  374. }
  375. function getNamespaceCond() {
  376. if( $this->namespace !== '' ) {
  377. return array( 'page_namespace' => (int)$this->namespace );
  378. } else {
  379. return array();
  380. }
  381. }
  382. function getIndexField() {
  383. return 'rev_timestamp';
  384. }
  385. function getStartBody() {
  386. return "<ul>\n";
  387. }
  388. function getEndBody() {
  389. return "</ul>\n";
  390. }
  391. /**
  392. * Generates each row in the contributions list.
  393. *
  394. * Contributions which are marked "top" are currently on top of the history.
  395. * For these contributions, a [rollback] link is shown for users with roll-
  396. * back privileges. The rollback link restores the most recent version that
  397. * was not written by the target user.
  398. *
  399. * @todo This would probably look a lot nicer in a table.
  400. */
  401. function formatRow( $row ) {
  402. global $wgLang, $wgUser, $wgContLang;
  403. wfProfileIn( __METHOD__ );
  404. $sk = $this->getSkin();
  405. $rev = new Revision( $row );
  406. $classes = array();
  407. $page = Title::newFromRow( $row );
  408. $page->resetArticleId( $row->rev_page ); // use process cache
  409. $link = $sk->makeLinkObj( $page, $page->getPrefixedText(), $page->isRedirect() ? 'redirect=no' : '' );
  410. # Mark current revisions
  411. $difftext = $topmarktext = '';
  412. if( $row->rev_id == $row->page_latest ) {
  413. $topmarktext .= '<strong>' . $this->messages['uctop'] . '</strong>';
  414. if( !$row->page_is_new ) {
  415. $difftext .= '(' . $sk->makeKnownLinkObj( $page, $this->messages['diff'], 'diff=0' ) . ')';
  416. # Add rollback link
  417. if( $page->quickUserCan( 'rollback') && $page->quickUserCan( 'edit' ) ) {
  418. $topmarktext .= ' '.$sk->generateRollback( $rev );
  419. }
  420. } else {
  421. $difftext .= $this->messages['newarticle'];
  422. }
  423. }
  424. # Is there a visible previous revision?
  425. if( $rev->userCan(Revision::DELETED_TEXT) ) {
  426. $difftext = '(' . $sk->makeKnownLinkObj( $page, $this->messages['diff'],
  427. 'diff=prev&oldid='.$row->rev_id ) . ')';
  428. } else {
  429. $difftext = '(' . $this->messages['diff'] . ')';
  430. }
  431. $histlink = '('.$sk->makeKnownLinkObj( $page, $this->messages['hist'], 'action=history' ) . ')';
  432. $comment = $wgContLang->getDirMark() . $sk->revComment( $rev, false, true );
  433. $date = $wgLang->timeanddate( wfTimestamp( TS_MW, $row->rev_timestamp ), true );
  434. $d = $sk->makeKnownLinkObj( $page, $date, 'oldid='.intval($row->rev_id) );
  435. if( $this->target == 'newbies' ) {
  436. $userlink = ' . . ' . $sk->userLink( $row->rev_user, $row->rev_user_text );
  437. $userlink .= ' (' . $sk->userTalkLink( $row->rev_user, $row->rev_user_text ) . ') ';
  438. } else {
  439. $userlink = '';
  440. }
  441. if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
  442. $d = '<span class="history-deleted">' . $d . '</span>';
  443. }
  444. if( $rev->getParentId() === 0 ) {
  445. $nflag = '<span class="newpage">' . $this->messages['newpageletter'] . '</span>';
  446. } else {
  447. $nflag = '';
  448. }
  449. if( $rev->isMinor() ) {
  450. $mflag = '<span class="minor">' . $this->messages['minoreditletter'] . '</span> ';
  451. } else {
  452. $mflag = '';
  453. }
  454. $ret = "{$d} {$histlink} {$difftext} {$nflag}{$mflag} {$link}{$userlink} {$comment} {$topmarktext}";
  455. if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
  456. $ret .= ' ' . wfMsgHtml( 'deletedrev' );
  457. }
  458. # Tags, if any.
  459. list($tagSummary, $newClasses) = ChangeTags::formatSummaryRow( $row->ts_tags, 'contributions' );
  460. $classes = array_merge( $classes, $newClasses );
  461. $ret .= " $tagSummary";
  462. // Let extensions add data
  463. wfRunHooks( 'ContributionsLineEnding', array( &$this, &$ret, $row ) );
  464. $classes = implode( ' ', $classes );
  465. $ret = "<li class=\"$classes\">$ret</li>\n";
  466. wfProfileOut( __METHOD__ );
  467. return $ret;
  468. }
  469. /**
  470. * Get the Database object in use
  471. *
  472. * @return Database
  473. */
  474. public function getDatabase() {
  475. return $this->mDb;
  476. }
  477. }