Document ContribsFinder and mark its members public/private (should cause no function...
[lhc/web/wiklou.git] / includes / SpecialContributions.php
1 <?php
2 /**
3 * @todo class-ify this as a SpecialPage
4 * @addtogroup SpecialPage
5 */
6
7 /**
8 * Finds the user's contributions in the database that match the specified
9 * offset, limit, and namespace.
10 *
11 * @addtogroup SpecialPage
12 */
13 class ContribsFinder {
14 private $username, $offset, $limit, $namespace;
15 private $dbr;
16
17 /**
18 * Constructor
19 * @param $username Username as a string
20 */
21 public function __construct( $username ) {
22 $this->username = $username;
23 $this->namespace = false;
24 $this->dbr = wfGetDB( DB_SLAVE, 'contributions' );
25 }
26
27 /**
28 * Mutator function to specify the namespace to be searched, if applicable
29 * @param $ns String: namespace
30 */
31 public function setNamespace( $ns ) {
32 $this->namespace = $ns;
33 }
34
35 /**
36 * Mutator function to specify the maximum number of contribs to return
37 * @param $limit Int
38 */
39 public function setLimit( $limit ) {
40 $this->limit = $limit;
41 }
42
43 /**
44 * Mutator function to specify the number of contribs to skip
45 * @param $offset Int
46 */
47 public function setOffset( $offset ) {
48 $this->offset = $offset;
49 }
50
51 /**
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).
55 */
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";
65
66 $res = $this->dbr->query( $sql, __METHOD__ );
67 $row = $this->dbr->fetchObject( $res );
68 if ( $row ) {
69 return wfTimestamp( TS_MW, $row->rev_timestamp );
70 } else {
71 return false;
72 }
73 }
74
75 /**
76 * Get timestamps of first and last contributions made by the user.
77 * @return Array containing first rev_timestamp and last rev_timestamp.
78 */
79 public function getEditLimits() {
80 return array(
81 $this->getEditLimit( "ASC" ),
82 $this->getEditLimit( "DESC" )
83 );
84 }
85
86 /**
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)
89 */
90 private function getUserCond() {
91 $condition = '';
92
93 if ( $this->username == 'newbies' ) {
94 $max = $this->dbr->selectField( 'user', 'max(user_id)', false, 'make_sql' );
95 $condition = '>' . (int)($max - $max / 100);
96 }
97
98 if ( $condition == '' ) {
99 $condition = ' rev_user_text=' . $this->dbr->addQuotes( $this->username );
100 $index = 'usertext_timestamp';
101 } else {
102 $condition = ' rev_user '.$condition ;
103 $index = 'user_timestamp';
104 }
105 return array( $index, $condition );
106 }
107
108 /**
109 * Get the namespace part of the WHERE clause for the query
110 * @return String: text to append to WHERE
111 */
112 private function getNamespaceCond() {
113 if ( $this->namespace !== false )
114 return ' AND page_namespace = ' . (int)$this->namespace;
115 return '';
116 }
117
118 /**
119 * @return Timestamp of first entry in previous page.
120 */
121 public function getPreviousOffsetForPaging() {
122 list( $index, $usercond ) = $this->getUserCond();
123 $nscond = $this->getNamespaceCond();
124
125 $use_index = $this->dbr->useIndexClause( $index );
126 list( $page, $revision ) = $this->dbr->tableNamesN( 'page', 'revision' );
127
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 " .
130 $usercond . $nscond;
131 $sql .= " ORDER BY rev_timestamp ASC";
132 $sql = $this->dbr->limitResult( $sql, $this->limit, 0 );
133 $res = $this->dbr->query( $sql );
134
135 $numRows = $this->dbr->numRows( $res );
136 if ( $numRows ) {
137 $this->dbr->dataSeek( $res, $numRows - 1 );
138 $row = $this->dbr->fetchObject( $res );
139 $offset = wfTimestamp( TS_MW, $row->rev_timestamp );
140 } else {
141 $offset = false;
142 }
143 $this->dbr->freeResult( $res );
144 return $offset;
145 }
146
147 /**
148 * @return Timestamp of first entry in next page.
149 */
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 " .
157 $usercond . $nscond;
158 $sql .= " ORDER BY rev_timestamp ASC";
159 $sql = $this->dbr->limitResult( $sql, $this->limit, 0 );
160 $res = $this->dbr->query( $sql );
161
162 $numRows = $this->dbr->numRows( $res );
163 if ( $numRows ) {
164 $this->dbr->dataSeek( $res, $numRows - 1 );
165 $row = $this->dbr->fetchObject( $res );
166 $offset = wfTimestamp( TS_MW, $row->rev_timestamp );
167 } else {
168 $offset = false;
169 }
170 $this->dbr->freeResult( $res );
171 return $offset;
172 }
173
174 /**
175 * Returns the SQL query to be used for this object
176 * @return String: SQL query
177 */
178 private function makeSql() {
179 $offsetQuery = '';
180
181 list( $page, $revision ) = $this->dbr->tableNamesN( 'page', 'revision' );
182 list( $index, $userCond ) = $this->getUserCond();
183
184 if ( $this->offset )
185 $offsetQuery = "AND rev_timestamp < '" . $this->dbr->timestamp($this->offset) . "'";
186
187 $nscond = $this->getNamespaceCond();
188 $use_index = $this->dbr->useIndexClause( $index );
189 $sql = 'SELECT ' .
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,'.
192 'rev_deleted ' .
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 );
197 return $sql;
198 }
199
200 /**
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.
204 */
205 public function find() {
206 $contribs = array();
207 $res = $this->dbr->query( $this->makeSql(), __METHOD__ );
208 while ( $c = $this->dbr->fetchObject( $res ) )
209 $contribs[] = $c;
210 $this->dbr->freeResult( $res );
211 return $contribs;
212 }
213 }
214
215 /**
216 * Special page "user contributions".
217 * Shows a list of the contributions of a user.
218 *
219 * @return none
220 * @param $par String: (optional) user name of the user for which to show the contributions
221 */
222 function wfSpecialContributions( $par = null ) {
223 global $wgUser, $wgOut, $wgLang, $wgRequest;
224
225 $options = array();
226
227 if ( isset( $par ) && $par == 'newbies' ) {
228 $target = 'newbies';
229 $options['contribs'] = 'newbie';
230 } elseif ( isset( $par ) ) {
231 $target = $par;
232 } else {
233 $target = ucfirst( $wgRequest->getVal( 'target' ) );
234 }
235
236 // check for radiobox
237 if ( $wgRequest->getVal( 'contribs' ) == 'newbie' ) {
238 $target = 'newbies';
239 $options['contribs'] = 'newbie';
240 }
241
242 if ( !strlen( $target ) ) {
243 $wgOut->addHTML( contributionsForm( '' ) );
244 return;
245 }
246
247 $nt = Title::newFromURL( $target );
248 if ( !$nt ) {
249 $wgOut->addHTML( contributionsForm( '' ) );
250 return;
251 }
252
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'] = '';
258 }
259
260 $title = SpecialPage::getTitleFor( 'Contributions' );
261 $options['target'] = $target;
262
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'] );
267
268 if ( ( $ns = $wgRequest->getVal( 'namespace', null ) ) !== null && $ns !== '' ) {
269 $options['namespace'] = intval( $ns );
270 $finder->setNamespace( $options['namespace'] );
271 } else {
272 $options['namespace'] = '';
273 }
274
275 if ( $wgUser->isAllowed( 'rollback' ) && $wgRequest->getBool( 'bot' ) ) {
276 $options['bot'] = '1';
277 }
278
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 );
285 return;
286 }
287 }
288
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 );
295 return;
296 }
297 }
298
299 if ( $target == 'newbies' ) {
300 $wgOut->setSubtitle( wfMsgHtml( 'sp-contributions-newbies-sub') );
301 } else {
302 $wgOut->setSubtitle( wfMsgHtml( 'contribsub', contributionsSub( $nt ) ) );
303 }
304
305 $id = User::idFromName( $nt->getText() );
306 wfRunHooks( 'SpecialContributionsBeforeMainOutput', $id );
307
308 $wgOut->addHTML( contributionsForm( $options) );
309
310 $contribs = $finder->find();
311
312 if ( count( $contribs ) == 0) {
313 $wgOut->addWikiText( wfMsg( 'nocontribs' ) );
314 return;
315 }
316
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 );
321
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'] );
327
328 if ( !$atstart ) {
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>";
333 }
334
335 if ( !$atend ) {
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>";
340 }
341
342 if ( $target == 'newbies' ) {
343 $firstlast ="($newestlink)";
344 } else {
345 $firstlast = "($newestlink | $oldestlink)";
346 }
347
348 $urls = array();
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>";
352 }
353 $bits = implode( $urls, ' | ' );
354
355 $prevnextbits = $firstlast .' '. wfMsgHtml( 'viewprevnext', $newerlink, $olderlink, $bits );
356
357 $wgOut->addHTML( "<p>{$prevnextbits}</p>\n" );
358
359 $wgOut->addHTML( "<ul>\n" );
360
361 $sk = $wgUser->getSkin();
362 foreach ( $contribs as $contrib )
363 $wgOut->addHTML( ucListEdit( $sk, $contrib ) );
364
365 $wgOut->addHTML( "</ul>\n" );
366 $wgOut->addHTML( "<p>{$prevnextbits}</p>\n" );
367
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>' );
379 }
380 }
381
382 }
383
384 /**
385 * Generates the subheading with links
386 * @param $nt @see Title object for the target
387 */
388 function contributionsSub( $nt ) {
389 global $wgSysopUserBans, $wgLang, $wgUser;
390
391 $sk = $wgUser->getSkin();
392 $id = User::idFromName( $nt->getText() );
393
394 if ( 0 == $id ) {
395 $ul = $nt->getText();
396 } else {
397 $ul = $sk->makeLinkObj( $nt, htmlspecialchars( $nt->getText() ) );
398 }
399 $talk = $nt->getTalkPage();
400 if( $talk ) {
401 # Talk page link
402 $tools[] = $sk->makeLinkObj( $talk, $wgLang->getNsText( NS_TALK ) );
403 if( ( $id != 0 && $wgSysopUserBans ) || ( $id == 0 && User::isIP( $nt->getText() ) ) ) {
404 # Block link
405 if( $wgUser->isAllowed( 'block' ) )
406 $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Blockip', $nt->getDBkey() ), wfMsgHtml( 'blocklink' ) );
407 # Block log link
408 $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), wfMsgHtml( 'sp-contributions-blocklog' ), 'type=block&page=' . $nt->getPrefixedUrl() );
409 }
410 # Other logs link
411 $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), wfMsgHtml( 'log' ), 'user=' . $nt->getPartialUrl() );
412 $ul .= ' (' . implode( ' | ', $tools ) . ')';
413 }
414 return $ul;
415 }
416
417 /**
418 * Generates the namespace selector form with hidden attributes.
419 * @param $options Array: the options to be included.
420 */
421 function contributionsForm( $options ) {
422 global $wgScript, $wgTitle, $wgRequest;
423
424 $options['title'] = $wgTitle->getPrefixedText();
425 if ( !isset( $options['target'] ) ) {
426 $options['target'] = '';
427 } else {
428 $options['target'] = str_replace( '_' , ' ' , $options['target'] );
429 }
430
431 if ( !isset( $options['namespace'] ) ) {
432 $options['namespace'] = '';
433 }
434
435 if ( !isset( $options['contribs'] ) ) {
436 $options['contribs'] = 'user';
437 }
438
439 if ( $options['contribs'] == 'newbie' ) {
440 $options['target'] = '';
441 }
442
443 $f = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) );
444
445 foreach ( $options as $name => $value ) {
446 if( $name === 'namespace' || $name === 'target' || $name === 'contribs' ) continue;
447 $f .= "\t" . Xml::hidden( $name, $value ) . "\n";
448 }
449
450 $f .= '<fieldset>' .
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' ) ) .
458 '</fieldset>' .
459 Xml::closeElement( 'form' );
460 return $f;
461 }
462
463 /**
464 * Generates each row in the contributions list.
465 *
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.
470 *
471 * @todo This would probably look a lot nicer in a table.
472 */
473 function ucListEdit( $sk, $row ) {
474 $fname = 'ucListEdit';
475 wfProfileIn( $fname );
476
477 global $wgLang, $wgUser, $wgRequest;
478 static $messages;
479 if( !isset( $messages ) ) {
480 foreach( explode( ' ', 'uctop diff newarticle rollbacklink diff hist minoreditletter' ) as $msg ) {
481 $messages[$msg] = wfMsgExt( $msg, array( 'escape') );
482 }
483 }
484
485 $rev = new Revision( $row );
486
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' ) . ')';
494 } else {
495 $difftext .= $messages['newarticle'];
496 }
497
498 if( $wgUser->isAllowed( 'rollback' ) ) {
499 $topmarktext .= ' '.$sk->generateRollback( $rev );
500 }
501
502 }
503 if( $rev->userCan( Revision::DELETED_TEXT ) ) {
504 $difftext = '(' . $sk->makeKnownLinkObj( $page, $messages['diff'], 'diff=prev&oldid='.$row->rev_id ) . ')';
505 } else {
506 $difftext = '(' . $messages['diff'] . ')';
507 }
508 $histlink='('.$sk->makeKnownLinkObj( $page, $messages['hist'], 'action=history' ) . ')';
509
510 $comment = $sk->revComment( $rev );
511 $d = $wgLang->timeanddate( wfTimestamp( TS_MW, $row->rev_timestamp ), true );
512
513 if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
514 $d = '<span class="history-deleted">' . $d . '</span>';
515 }
516
517 if( $row->rev_minor_edit ) {
518 $mflag = '<span class="minor">' . $messages['minoreditletter'] . '</span> ';
519 } else {
520 $mflag = '';
521 }
522
523 $ret = "{$d} {$histlink} {$difftext} {$mflag} {$link} {$comment} {$topmarktext}";
524 if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
525 $ret .= ' ' . wfMsgHtml( 'deletedrev' );
526 }
527 $ret = "<li>$ret</li>\n";
528 wfProfileOut( $fname );
529 return $ret;
530 }
531
532 ?>