3 * @todo class-ify this as a SpecialPage
4 * @addtogroup SpecialPage
8 * Finds the user's contributions in the database that match the specified
9 * offset, limit, and namespace.
11 * @addtogroup SpecialPage
13 class ContribsFinder
{
14 private $username, $offset, $limit, $namespace;
19 * @param $username Username as a string
21 public function __construct( $username ) {
22 $this->username
= $username;
23 $this->namespace = false;
24 $this->dbr
= wfGetDB( DB_SLAVE
, 'contributions' );
28 * Mutator function to specify the namespace to be searched, if applicable
29 * @param $ns String: namespace
31 public function setNamespace( $ns ) {
32 $this->namespace = $ns;
36 * Mutator function to specify the maximum number of contribs to return
39 public function setLimit( $limit ) {
40 $this->limit
= $limit;
44 * Mutator function to specify the number of contribs to skip
47 public function setOffset( $offset ) {
48 $this->offset
= $offset;
52 * Get timestamp of either first or last contribution made by the user.
53 * @param $dir string 'ASC' or 'DESC'.
54 * @return Revision timestamp (rev_timestamp).
56 private function getEditLimit( $dir ) {
57 list( $index, $usercond ) = $this->getUserCond();
58 $nscond = $this->getNamespaceCond();
59 $use_index = $this->dbr
->useIndexClause( $index );
60 list( $revision, $page) = $this->dbr
->tableNamesN( 'revision', 'page' );
61 $sql = "SELECT rev_timestamp " .
62 " FROM $page,$revision $use_index " .
63 " WHERE rev_page=page_id AND $usercond $nscond" .
64 " ORDER BY rev_timestamp $dir LIMIT 1";
66 $res = $this->dbr
->query( $sql, __METHOD__
);
67 $row = $this->dbr
->fetchObject( $res );
69 return wfTimestamp( TS_MW
, $row->rev_timestamp
);
76 * Get timestamps of first and last contributions made by the user.
77 * @return Array containing first rev_timestamp and last rev_timestamp.
79 public function getEditLimits() {
81 $this->getEditLimit( "ASC" ),
82 $this->getEditLimit( "DESC" )
87 * Get the user part of the WHERE clause for the query
88 * @return Array of strings: (index to use, text to append to WHERE)
90 private function getUserCond() {
93 if ( $this->username
== 'newbies' ) {
94 $max = $this->dbr
->selectField( 'user', 'max(user_id)', false, 'make_sql' );
95 $condition = '>' . (int)($max - $max / 100);
98 if ( $condition == '' ) {
99 $condition = ' rev_user_text=' . $this->dbr
->addQuotes( $this->username
);
100 $index = 'usertext_timestamp';
102 $condition = ' rev_user '.$condition ;
103 $index = 'user_timestamp';
105 return array( $index, $condition );
109 * Get the namespace part of the WHERE clause for the query
110 * @return String: text to append to WHERE
112 private function getNamespaceCond() {
113 if ( $this->namespace !== false )
114 return ' AND page_namespace = ' . (int)$this->namespace;
119 * @return Timestamp of first entry in previous page.
121 public function getPreviousOffsetForPaging() {
122 list( $index, $usercond ) = $this->getUserCond();
123 $nscond = $this->getNamespaceCond();
125 $use_index = $this->dbr
->useIndexClause( $index );
126 list( $page, $revision ) = $this->dbr
->tableNamesN( 'page', 'revision' );
128 $sql = "SELECT rev_timestamp FROM $page, $revision $use_index " .
129 "WHERE page_id = rev_page AND rev_timestamp > '" . $this->dbr
->timestamp( $this->offset
) . "' AND " .
131 $sql .= " ORDER BY rev_timestamp ASC";
132 $sql = $this->dbr
->limitResult( $sql, $this->limit
, 0 );
133 $res = $this->dbr
->query( $sql );
135 $numRows = $this->dbr
->numRows( $res );
137 $this->dbr
->dataSeek( $res, $numRows - 1 );
138 $row = $this->dbr
->fetchObject( $res );
139 $offset = wfTimestamp( TS_MW
, $row->rev_timestamp
);
143 $this->dbr
->freeResult( $res );
148 * @return Timestamp of first entry in next page.
150 public function getFirstOffsetForPaging() {
151 list( $index, $usercond ) = $this->getUserCond();
152 $use_index = $this->dbr
->useIndexClause( $index );
153 list( $page, $revision ) = $this->dbr
->tableNamesN( 'page', 'revision' );
154 $nscond = $this->getNamespaceCond();
155 $sql = "SELECT rev_timestamp FROM $page, $revision $use_index " .
156 "WHERE page_id = rev_page AND " .
158 $sql .= " ORDER BY rev_timestamp ASC";
159 $sql = $this->dbr
->limitResult( $sql, $this->limit
, 0 );
160 $res = $this->dbr
->query( $sql );
162 $numRows = $this->dbr
->numRows( $res );
164 $this->dbr
->dataSeek( $res, $numRows - 1 );
165 $row = $this->dbr
->fetchObject( $res );
166 $offset = wfTimestamp( TS_MW
, $row->rev_timestamp
);
170 $this->dbr
->freeResult( $res );
175 * Returns the SQL query to be used for this object
176 * @return String: SQL query
178 private function makeSql() {
181 list( $page, $revision ) = $this->dbr
->tableNamesN( 'page', 'revision' );
182 list( $index, $userCond ) = $this->getUserCond();
185 $offsetQuery = "AND rev_timestamp < '" . $this->dbr
->timestamp($this->offset
) . "'";
187 $nscond = $this->getNamespaceCond();
188 $use_index = $this->dbr
->useIndexClause( $index );
190 'page_namespace,page_title,page_is_new,page_latest,'.
191 'rev_id,rev_page,rev_text_id,rev_timestamp,rev_comment,rev_minor_edit,rev_user,rev_user_text,'.
193 "FROM $page,$revision $use_index " .
194 "WHERE page_id=rev_page AND $userCond $nscond $offsetQuery " .
195 'ORDER BY rev_timestamp DESC';
196 $sql = $this->dbr
->limitResult( $sql, $this->limit
, 0 );
201 * This do the search for the user given when creating the object.
202 * @todo Consider writing this be the only public function in this class?
203 * @return Array of contributions.
205 public function find() {
207 $res = $this->dbr
->query( $this->makeSql(), __METHOD__
);
208 while ( $c = $this->dbr
->fetchObject( $res ) )
210 $this->dbr
->freeResult( $res );
216 * Special page "user contributions".
217 * Shows a list of the contributions of a user.
220 * @param $par String: (optional) user name of the user for which to show the contributions
222 function wfSpecialContributions( $par = null ) {
223 global $wgUser, $wgOut, $wgLang, $wgRequest;
227 if ( isset( $par ) && $par == 'newbies' ) {
229 $options['contribs'] = 'newbie';
230 } elseif ( isset( $par ) ) {
233 $target = ucfirst( $wgRequest->getVal( 'target' ) );
236 // check for radiobox
237 if ( $wgRequest->getVal( 'contribs' ) == 'newbie' ) {
239 $options['contribs'] = 'newbie';
242 if ( !strlen( $target ) ) {
243 $wgOut->addHTML( contributionsForm( '' ) );
247 $nt = Title
::newFromURL( $target );
249 $wgOut->addHTML( contributionsForm( '' ) );
253 list( $options['limit'], $options['offset']) = wfCheckLimits();
254 $options['offset'] = $wgRequest->getVal( 'offset' );
255 /* Offset must be an integral. */
256 if ( !strlen( $options['offset'] ) ||
!preg_match( '/^[0-9]+$/', $options['offset'] ) ) {
257 $options['offset'] = '';
260 $title = SpecialPage
::getTitleFor( 'Contributions' );
261 $options['target'] = $target;
263 $nt = Title
::makeTitle( NS_USER
, $nt->getDBkey() );
264 $finder = new ContribsFinder( ( $target == 'newbies' ) ?
'newbies' : $nt->getText() );
265 $finder->setLimit( $options['limit'] );
266 $finder->setOffset( $options['offset'] );
268 if ( ( $ns = $wgRequest->getVal( 'namespace', null ) ) !== null && $ns !== '' ) {
269 $options['namespace'] = intval( $ns );
270 $finder->setNamespace( $options['namespace'] );
272 $options['namespace'] = '';
275 if ( $wgUser->isAllowed( 'rollback' ) && $wgRequest->getBool( 'bot' ) ) {
276 $options['bot'] = '1';
279 if ( $wgRequest->getText( 'go' ) == 'prev' ) {
280 $offset = $finder->getPreviousOffsetForPaging();
281 if ( $offset !== false ) {
282 $options['offset'] = $offset;
283 $prevurl = $title->getLocalURL( wfArrayToCGI( $options ) );
284 $wgOut->redirect( $prevurl );
289 if ( $wgRequest->getText( 'go' ) == 'first' && $target != 'newbies') {
290 $offset = $finder->getFirstOffsetForPaging();
291 if ( $offset !== false ) {
292 $options['offset'] = $offset;
293 $prevurl = $title->getLocalURL( wfArrayToCGI( $options ) );
294 $wgOut->redirect( $prevurl );
299 if ( $target == 'newbies' ) {
300 $wgOut->setSubtitle( wfMsgHtml( 'sp-contributions-newbies-sub') );
302 $wgOut->setSubtitle( wfMsgHtml( 'contribsub', contributionsSub( $nt ) ) );
305 $id = User
::idFromName( $nt->getText() );
306 wfRunHooks( 'SpecialContributionsBeforeMainOutput', $id );
308 $wgOut->addHTML( contributionsForm( $options) );
310 $contribs = $finder->find();
312 if ( count( $contribs ) == 0) {
313 $wgOut->addWikiText( wfMsg( 'nocontribs' ) );
317 list( $early, $late ) = $finder->getEditLimits();
318 $lastts = count( $contribs ) ?
wfTimestamp( TS_MW
, $contribs[count( $contribs ) - 1]->rev_timestamp
) : 0;
319 $atstart = ( !count( $contribs ) ||
$late == wfTimestamp( TS_MW
, $contribs[0]->rev_timestamp
) );
320 $atend = ( !count( $contribs ) ||
$early == $lastts );
322 // These four are defaults
323 $newestlink = wfMsgHtml( 'sp-contributions-newest' );
324 $oldestlink = wfMsgHtml( 'sp-contributions-oldest' );
325 $newerlink = wfMsgHtml( 'sp-contributions-newer', $options['limit'] );
326 $olderlink = wfMsgHtml( 'sp-contributions-older', $options['limit'] );
329 $stuff = $title->escapeLocalURL( wfArrayToCGI( array( 'offset' => '' ), $options ) );
330 $newestlink = "<a href=\"$stuff\">$newestlink</a>";
331 $stuff = $title->escapeLocalURL( wfArrayToCGI( array( 'go' => 'prev' ), $options ) );
332 $newerlink = "<a href=\"$stuff\">$newerlink</a>";
336 $stuff = $title->escapeLocalURL( wfArrayToCGI( array( 'go' => 'first' ), $options ) );
337 $oldestlink = "<a href=\"$stuff\">$oldestlink</a>";
338 $stuff = $title->escapeLocalURL( wfArrayToCGI( array( 'offset' => $lastts ), $options ) );
339 $olderlink = "<a href=\"$stuff\">$olderlink</a>";
342 if ( $target == 'newbies' ) {
343 $firstlast ="($newestlink)";
345 $firstlast = "($newestlink | $oldestlink)";
349 foreach ( array( 20, 50, 100, 250, 500 ) as $num ) {
350 $stuff = $title->escapeLocalURL( wfArrayToCGI( array( 'limit' => $num ), $options ) );
351 $urls[] = "<a href=\"$stuff\">".$wgLang->formatNum( $num )."</a>";
353 $bits = implode( $urls, ' | ' );
355 $prevnextbits = $firstlast .' '. wfMsgHtml( 'viewprevnext', $newerlink, $olderlink, $bits );
357 $wgOut->addHTML( "<p>{$prevnextbits}</p>\n" );
359 $wgOut->addHTML( "<ul>\n" );
361 $sk = $wgUser->getSkin();
362 foreach ( $contribs as $contrib )
363 $wgOut->addHTML( ucListEdit( $sk, $contrib ) );
365 $wgOut->addHTML( "</ul>\n" );
366 $wgOut->addHTML( "<p>{$prevnextbits}</p>\n" );
368 # If there were contributions, and it was a valid user or IP, show
369 # the appropriate "footer" message - WHOIS tools, etc.
370 if( count( $contribs ) > 0 && $target != 'newbies' && $nt instanceof Title
) {
371 $message = IP
::isIPAddress( $target )
372 ?
'sp-contributions-footer-anon'
373 : 'sp-contributions-footer';
374 $text = wfMsg( $message, $target );
375 if( !wfEmptyMsg( $message, $text ) && $text != '-' ) {
376 $wgOut->addHtml( '<div class="mw-contributions-footer">' );
377 $wgOut->addWikiText( wfMsg( $message, $target ) );
378 $wgOut->addHtml( '</div>' );
385 * Generates the subheading with links
386 * @param $nt @see Title object for the target
388 function contributionsSub( $nt ) {
389 global $wgSysopUserBans, $wgLang, $wgUser;
391 $sk = $wgUser->getSkin();
392 $id = User
::idFromName( $nt->getText() );
395 $ul = $nt->getText();
397 $ul = $sk->makeLinkObj( $nt, htmlspecialchars( $nt->getText() ) );
399 $talk = $nt->getTalkPage();
402 $tools[] = $sk->makeLinkObj( $talk, $wgLang->getNsText( NS_TALK
) );
403 if( ( $id != 0 && $wgSysopUserBans ) ||
( $id == 0 && User
::isIP( $nt->getText() ) ) ) {
405 if( $wgUser->isAllowed( 'block' ) )
406 $tools[] = $sk->makeKnownLinkObj( SpecialPage
::getTitleFor( 'Blockip', $nt->getDBkey() ), wfMsgHtml( 'blocklink' ) );
408 $tools[] = $sk->makeKnownLinkObj( SpecialPage
::getTitleFor( 'Log' ), wfMsgHtml( 'sp-contributions-blocklog' ), 'type=block&page=' . $nt->getPrefixedUrl() );
411 $tools[] = $sk->makeKnownLinkObj( SpecialPage
::getTitleFor( 'Log' ), wfMsgHtml( 'log' ), 'user=' . $nt->getPartialUrl() );
412 $ul .= ' (' . implode( ' | ', $tools ) . ')';
418 * Generates the namespace selector form with hidden attributes.
419 * @param $options Array: the options to be included.
421 function contributionsForm( $options ) {
422 global $wgScript, $wgTitle, $wgRequest;
424 $options['title'] = $wgTitle->getPrefixedText();
425 if ( !isset( $options['target'] ) ) {
426 $options['target'] = '';
428 $options['target'] = str_replace( '_' , ' ' , $options['target'] );
431 if ( !isset( $options['namespace'] ) ) {
432 $options['namespace'] = '';
435 if ( !isset( $options['contribs'] ) ) {
436 $options['contribs'] = 'user';
439 if ( $options['contribs'] == 'newbie' ) {
440 $options['target'] = '';
443 $f = Xml
::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) );
445 foreach ( $options as $name => $value ) {
446 if( $name === 'namespace' ||
$name === 'target' ||
$name === 'contribs' ) continue;
447 $f .= "\t" . Xml
::hidden( $name, $value ) . "\n";
451 Xml
::element( 'legend', array(), wfMsg( 'sp-contributions-search' ) ) .
452 Xml
::radioLabel( wfMsgExt( 'sp-contributions-newbies', array( 'parseinline' ) ), 'contribs' , 'newbie' , 'newbie', $options['contribs'] == 'newbie' ?
true : false ) . '<br />' .
453 Xml
::radioLabel( wfMsgExt( 'sp-contributions-username', array( 'parseinline' ) ), 'contribs' , 'user', 'user', $options['contribs'] == 'user' ?
true : false ) . ' ' .
454 Xml
::input( 'target', 20, $options['target']) . ' '.
455 Xml
::label( wfMsg( 'namespace' ), 'namespace' ) .
456 Xml
::namespaceSelector( $options['namespace'], '' ) .
457 Xml
::submitButton( wfMsg( 'sp-contributions-submit' ) ) .
459 Xml
::closeElement( 'form' );
464 * Generates each row in the contributions list.
466 * Contributions which are marked "top" are currently on top of the history.
467 * For these contributions, a [rollback] link is shown for users with sysop
468 * privileges. The rollback link restores the most recent version that was not
469 * written by the target user.
471 * @todo This would probably look a lot nicer in a table.
473 function ucListEdit( $sk, $row ) {
474 $fname = 'ucListEdit';
475 wfProfileIn( $fname );
477 global $wgLang, $wgUser, $wgRequest;
479 if( !isset( $messages ) ) {
480 foreach( explode( ' ', 'uctop diff newarticle rollbacklink diff hist minoreditletter' ) as $msg ) {
481 $messages[$msg] = wfMsgExt( $msg, array( 'escape') );
485 $rev = new Revision( $row );
487 $page = Title
::makeTitle( $row->page_namespace
, $row->page_title
);
488 $link = $sk->makeKnownLinkObj( $page );
489 $difftext = $topmarktext = '';
490 if( $row->rev_id
== $row->page_latest
) {
491 $topmarktext .= '<strong>' . $messages['uctop'] . '</strong>';
492 if( !$row->page_is_new
) {
493 $difftext .= '(' . $sk->makeKnownLinkObj( $page, $messages['diff'], 'diff=0' ) . ')';
495 $difftext .= $messages['newarticle'];
498 if( $wgUser->isAllowed( 'rollback' ) ) {
499 $topmarktext .= ' '.$sk->generateRollback( $rev );
503 if( $rev->userCan( Revision
::DELETED_TEXT
) ) {
504 $difftext = '(' . $sk->makeKnownLinkObj( $page, $messages['diff'], 'diff=prev&oldid='.$row->rev_id
) . ')';
506 $difftext = '(' . $messages['diff'] . ')';
508 $histlink='('.$sk->makeKnownLinkObj( $page, $messages['hist'], 'action=history' ) . ')';
510 $comment = $sk->revComment( $rev );
511 $d = $wgLang->timeanddate( wfTimestamp( TS_MW
, $row->rev_timestamp
), true );
513 if( $rev->isDeleted( Revision
::DELETED_TEXT
) ) {
514 $d = '<span class="history-deleted">' . $d . '</span>';
517 if( $row->rev_minor_edit
) {
518 $mflag = '<span class="minor">' . $messages['minoreditletter'] . '</span> ';
523 $ret = "{$d} {$histlink} {$difftext} {$mflag} {$link} {$comment} {$topmarktext}";
524 if( $rev->isDeleted( Revision
::DELETED_TEXT
) ) {
525 $ret .= ' ' . wfMsgHtml( 'deletedrev' );
527 $ret = "<li>$ret</li>\n";
528 wfProfileOut( $fname );