Merge "Avoid showing crazy staleness times at ActiveUsers"
[lhc/web/wiklou.git] / includes / specialpage / ChangesListSpecialPage.php
1 <?php
2 /**
3 * Special page which uses a ChangesList to show query results.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup SpecialPage
22 */
23
24 /**
25 * Special page which uses a ChangesList to show query results.
26 * @todo Way too many public functions, most of them should be protected
27 *
28 * @ingroup SpecialPage
29 */
30 abstract class ChangesListSpecialPage extends SpecialPage {
31 /** @var string */
32 protected $rcSubpage;
33
34 /** @var FormOptions */
35 protected $rcOptions;
36
37 /** @var array */
38 protected $customFilters;
39
40 /**
41 * Main execution point
42 *
43 * @param string $subpage
44 */
45 public function execute( $subpage ) {
46 $this->rcSubpage = $subpage;
47
48 $this->setHeaders();
49 $this->outputHeader();
50 $this->addModules();
51
52 $rows = $this->getRows();
53 $opts = $this->getOptions();
54 if ( $rows === false ) {
55 if ( !$this->including() ) {
56 $this->doHeader( $opts );
57 }
58
59 return;
60 }
61
62 $batch = new LinkBatch;
63 foreach ( $rows as $row ) {
64 $batch->add( NS_USER, $row->rc_user_text );
65 $batch->add( NS_USER_TALK, $row->rc_user_text );
66 $batch->add( $row->rc_namespace, $row->rc_title );
67 }
68 $batch->execute();
69
70 $this->webOutput( $rows, $opts );
71
72 $rows->free();
73 }
74
75 /**
76 * Get the database result for this special page instance. Used by ApiFeedRecentChanges.
77 *
78 * @return bool|ResultWrapper Result or false
79 */
80 public function getRows() {
81 $opts = $this->getOptions();
82 $conds = $this->buildMainQueryConds( $opts );
83 return $this->doMainQuery( $conds, $opts );
84 }
85
86 /**
87 * Get the current FormOptions for this request
88 *
89 * @return FormOptions
90 */
91 public function getOptions() {
92 if ( $this->rcOptions === null ) {
93 $this->rcOptions = $this->setup( $this->rcSubpage );
94 }
95
96 return $this->rcOptions;
97 }
98
99 /**
100 * Create a FormOptions object with options as specified by the user
101 *
102 * @param array $parameters
103 *
104 * @return FormOptions
105 */
106 public function setup( $parameters ) {
107 $opts = $this->getDefaultOptions();
108 foreach ( $this->getCustomFilters() as $key => $params ) {
109 $opts->add( $key, $params['default'] );
110 }
111
112 $opts = $this->fetchOptionsFromRequest( $opts );
113
114 // Give precedence to subpage syntax
115 if ( $parameters !== null ) {
116 $this->parseParameters( $parameters, $opts );
117 }
118
119 $this->validateOptions( $opts );
120
121 return $opts;
122 }
123
124 /**
125 * Get a FormOptions object containing the default options. By default returns some basic options,
126 * you might want to not call parent method and discard them, or to override default values.
127 *
128 * @return FormOptions
129 */
130 public function getDefaultOptions() {
131 $opts = new FormOptions();
132
133 $opts->add( 'hideminor', false );
134 $opts->add( 'hidebots', false );
135 $opts->add( 'hideanons', false );
136 $opts->add( 'hideliu', false );
137 $opts->add( 'hidepatrolled', false );
138 $opts->add( 'hidemyself', false );
139
140 $opts->add( 'namespace', '', FormOptions::INTNULL );
141 $opts->add( 'invert', false );
142 $opts->add( 'associated', false );
143
144 return $opts;
145 }
146
147 /**
148 * Get custom show/hide filters
149 *
150 * @return array Map of filter URL param names to properties (msg/default)
151 */
152 protected function getCustomFilters() {
153 // @todo Fire a Special{$this->getName()}Filters hook here
154 return array();
155 }
156
157 /**
158 * Fetch values for a FormOptions object from the WebRequest associated with this instance.
159 *
160 * Intended for subclassing, e.g. to add a backwards-compatibility layer.
161 *
162 * @param FormOptions $parameters
163 * @return FormOptions
164 */
165 protected function fetchOptionsFromRequest( $opts ) {
166 $opts->fetchValuesFromRequest( $this->getRequest() );
167 return $opts;
168 }
169
170 /**
171 * Process $par and put options found in $opts. Used when including the page.
172 *
173 * @param string $par
174 * @param FormOptions $opts
175 */
176 public function parseParameters( $par, FormOptions $opts ) {
177 // nothing by default
178 }
179
180 /**
181 * Validate a FormOptions object generated by getDefaultOptions() with values already populated.
182 *
183 * @param FormOptions $opts
184 */
185 public function validateOptions( FormOptions $opts ) {
186 // nothing by default
187 }
188
189 /**
190 * Return an array of conditions depending of options set in $opts
191 *
192 * @param FormOptions $opts
193 * @return array
194 */
195 public function buildMainQueryConds( FormOptions $opts ) {
196 $dbr = $this->getDB();
197 $user = $this->getUser();
198 $conds = array();
199
200 // It makes no sense to hide both anons and logged-in users. When this occurs, try a guess on
201 // what the user meant and either show only bots or force anons to be shown.
202 $botsonly = false;
203 $hideanons = $opts['hideanons'];
204 if ( $opts['hideanons'] && $opts['hideliu'] ) {
205 if ( $opts['hidebots'] ) {
206 $hideanons = false;
207 } else {
208 $botsonly = true;
209 }
210 }
211
212 // Toggles
213 if ( $opts['hideminor'] ) {
214 $conds['rc_minor'] = 0;
215 }
216 if ( $opts['hidebots'] ) {
217 $conds['rc_bot'] = 0;
218 }
219 if ( $user->useRCPatrol() && $opts['hidepatrolled'] ) {
220 $conds['rc_patrolled'] = 0;
221 }
222 if ( $botsonly ) {
223 $conds['rc_bot'] = 1;
224 } else {
225 if ( $opts['hideliu'] ) {
226 $conds[] = 'rc_user = 0';
227 }
228 if ( $hideanons ) {
229 $conds[] = 'rc_user != 0';
230 }
231 }
232 if ( $opts['hidemyself'] ) {
233 if ( $user->getId() ) {
234 $conds[] = 'rc_user != ' . $dbr->addQuotes( $user->getId() );
235 } else {
236 $conds[] = 'rc_user_text != ' . $dbr->addQuotes( $user->getName() );
237 }
238 }
239
240 // Namespace filtering
241 if ( $opts['namespace'] !== '' ) {
242 $selectedNS = $dbr->addQuotes( $opts['namespace'] );
243 $operator = $opts['invert'] ? '!=' : '=';
244 $boolean = $opts['invert'] ? 'AND' : 'OR';
245
246 // Namespace association (bug 2429)
247 if ( !$opts['associated'] ) {
248 $condition = "rc_namespace $operator $selectedNS";
249 } else {
250 // Also add the associated namespace
251 $associatedNS = $dbr->addQuotes(
252 MWNamespace::getAssociated( $opts['namespace'] )
253 );
254 $condition = "(rc_namespace $operator $selectedNS "
255 . $boolean
256 . " rc_namespace $operator $associatedNS)";
257 }
258
259 $conds[] = $condition;
260 }
261
262 return $conds;
263 }
264
265 /**
266 * Process the query
267 *
268 * @param array $conds
269 * @param FormOptions $opts
270 * @return bool|ResultWrapper Result or false
271 */
272 public function doMainQuery( $conds, $opts ) {
273 $tables = array( 'recentchanges' );
274 $fields = RecentChange::selectFields();
275 $query_options = array();
276 $join_conds = array();
277
278 ChangeTags::modifyDisplayQuery(
279 $tables,
280 $fields,
281 $conds,
282 $join_conds,
283 $query_options,
284 ''
285 );
286
287 // @todo Fire a Special{$this->getName()}Query hook here
288 // @todo Uncomment and document
289 // if ( !wfRunHooks( 'ChangesListSpecialPageQuery',
290 // array( &$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts ) )
291 // ) {
292 // return false;
293 // }
294
295 $dbr = $this->getDB();
296 return $dbr->select(
297 $tables,
298 $fields,
299 $conds,
300 __METHOD__,
301 $query_options,
302 $join_conds
303 );
304 }
305
306 /**
307 * Return a DatabaseBase object for reading
308 *
309 * @return DatabaseBase
310 */
311 protected function getDB() {
312 return wfGetDB( DB_SLAVE );
313 }
314
315 /**
316 * Send output to the OutputPage object, only called if not used feeds
317 *
318 * @param ResultWrapper $rows Database rows
319 * @param FormOptions $opts
320 */
321 public function webOutput( $rows, $opts ) {
322 if ( !$this->including() ) {
323 $this->outputFeedLinks();
324 $this->doHeader( $opts );
325 }
326
327 $this->outputChangesList( $rows, $opts );
328 }
329
330 /**
331 * Output feed links.
332 */
333 public function outputFeedLinks() {
334 // nothing by default
335 }
336
337 /**
338 * Build and output the actual changes list.
339 *
340 * @param array $rows Database rows
341 * @param FormOptions $opts
342 */
343 abstract public function outputChangesList( $rows, $opts );
344
345 /**
346 * Return the text to be displayed above the changes
347 *
348 * @param FormOptions $opts
349 * @return string XHTML
350 */
351 public function doHeader( $opts ) {
352 $this->setTopText( $opts );
353
354 // @todo Lots of stuff should be done here.
355
356 $this->setBottomText( $opts );
357 }
358
359 /**
360 * Send the text to be displayed before the options. Should use $this->getOutput()->addWikiText()
361 * or similar methods to print the text.
362 *
363 * @param FormOptions $opts
364 */
365 function setTopText( FormOptions $opts ) {
366 // nothing by default
367 }
368
369 /**
370 * Send the text to be displayed after the options. Should use $this->getOutput()->addWikiText()
371 * or similar methods to print the text.
372 *
373 * @param FormOptions $opts
374 */
375 function setBottomText( FormOptions $opts ) {
376 // nothing by default
377 }
378
379 /**
380 * Get options to be displayed in a form
381 * @todo This should handle options returned by getDefaultOptions().
382 * @todo Not called by anything, should be called by something… doHeader() maybe?
383 *
384 * @param FormOptions $opts
385 * @return array
386 */
387 function getExtraOptions( $opts ) {
388 return array();
389 }
390
391 /**
392 * Return the legend displayed within the fieldset
393 * @todo This should not be static, then we can drop the parameter
394 * @todo Not called by anything, should be called by doHeader()
395 *
396 * @param $context the object available as $this in non-static functions
397 * @return string
398 */
399 public static function makeLegend( IContextSource $context ) {
400 global $wgRecentChangesFlags;
401 $user = $context->getUser();
402 # The legend showing what the letters and stuff mean
403 $legend = Xml::openElement( 'dl' ) . "\n";
404 # Iterates through them and gets the messages for both letter and tooltip
405 $legendItems = $wgRecentChangesFlags;
406 if ( !$user->useRCPatrol() ) {
407 unset( $legendItems['unpatrolled'] );
408 }
409 foreach ( $legendItems as $key => $legendInfo ) { # generate items of the legend
410 $label = $legendInfo['title'];
411 $letter = $legendInfo['letter'];
412 $cssClass = isset( $legendInfo['class'] ) ? $legendInfo['class'] : $key;
413
414 $legend .= Xml::element( 'dt',
415 array( 'class' => $cssClass ), $context->msg( $letter )->text()
416 ) . "\n";
417 if ( $key === 'newpage' ) {
418 $legend .= Xml::openElement( 'dd' );
419 $legend .= $context->msg( $label )->escaped();
420 $legend .= ' ' . $context->msg( 'recentchanges-legend-newpage' )->parse();
421 $legend .= Xml::closeElement( 'dd' ) . "\n";
422 } else {
423 $legend .= Xml::element( 'dd', array(),
424 $context->msg( $label )->text()
425 ) . "\n";
426 }
427 }
428 # (+-123)
429 $legend .= Xml::tags( 'dt',
430 array( 'class' => 'mw-plusminus-pos' ),
431 $context->msg( 'recentchanges-legend-plusminus' )->parse()
432 ) . "\n";
433 $legend .= Xml::element(
434 'dd',
435 array( 'class' => 'mw-changeslist-legend-plusminus' ),
436 $context->msg( 'recentchanges-label-plusminus' )->text()
437 ) . "\n";
438 $legend .= Xml::closeElement( 'dl' ) . "\n";
439
440 # Collapsibility
441 $legend =
442 '<div class="mw-changeslist-legend">' .
443 $context->msg( 'recentchanges-legend-heading' )->parse() .
444 '<div class="mw-collapsible-content">' . $legend . '</div>' .
445 '</div>';
446
447 return $legend;
448 }
449
450 /**
451 * Add page-specific modules.
452 */
453 protected function addModules() {
454 $out = $this->getOutput();
455 // Styles and behavior for the legend box (see makeLegend())
456 $out->addModuleStyles( 'mediawiki.special.changeslist.legend' );
457 $out->addModules( 'mediawiki.special.changeslist.legend.js' );
458 }
459
460 protected function getGroupName() {
461 return 'changes';
462 }
463 }