Merge "API: Use message-per-value for apihelp-query+allcategories-param-prop"
[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, 0 );
57 $this->getOutput()->setStatusCode( 404 );
58 }
59
60 return;
61 }
62
63 $batch = new LinkBatch;
64 foreach ( $rows as $row ) {
65 $batch->add( NS_USER, $row->rc_user_text );
66 $batch->add( NS_USER_TALK, $row->rc_user_text );
67 $batch->add( $row->rc_namespace, $row->rc_title );
68 if ( $row->rc_source === RecentChange::SRC_LOG ) {
69 $formatter = LogFormatter::newFromRow( $row );
70 foreach ( $formatter->getPreloadTitles() as $title ) {
71 $batch->addObj( $title );
72 }
73 }
74 }
75 $batch->execute();
76
77 $this->webOutput( $rows, $opts );
78
79 $rows->free();
80 }
81
82 /**
83 * Get the database result for this special page instance. Used by ApiFeedRecentChanges.
84 *
85 * @return bool|ResultWrapper Result or false
86 */
87 public function getRows() {
88 $opts = $this->getOptions();
89 $conds = $this->buildMainQueryConds( $opts );
90
91 return $this->doMainQuery( $conds, $opts );
92 }
93
94 /**
95 * Get the current FormOptions for this request
96 *
97 * @return FormOptions
98 */
99 public function getOptions() {
100 if ( $this->rcOptions === null ) {
101 $this->rcOptions = $this->setup( $this->rcSubpage );
102 }
103
104 return $this->rcOptions;
105 }
106
107 /**
108 * Create a FormOptions object with options as specified by the user
109 *
110 * @param array $parameters
111 *
112 * @return FormOptions
113 */
114 public function setup( $parameters ) {
115 $opts = $this->getDefaultOptions();
116 foreach ( $this->getCustomFilters() as $key => $params ) {
117 $opts->add( $key, $params['default'] );
118 }
119
120 $opts = $this->fetchOptionsFromRequest( $opts );
121
122 // Give precedence to subpage syntax
123 if ( $parameters !== null ) {
124 $this->parseParameters( $parameters, $opts );
125 }
126
127 $this->validateOptions( $opts );
128
129 return $opts;
130 }
131
132 /**
133 * Get a FormOptions object containing the default options. By default returns some basic options,
134 * you might want to not call parent method and discard them, or to override default values.
135 *
136 * @return FormOptions
137 */
138 public function getDefaultOptions() {
139 $opts = new FormOptions();
140
141 $opts->add( 'hideminor', false );
142 $opts->add( 'hidebots', false );
143 $opts->add( 'hideanons', false );
144 $opts->add( 'hideliu', false );
145 $opts->add( 'hidepatrolled', false );
146 $opts->add( 'hidemyself', false );
147 $opts->add( 'hidecategorization', false );
148
149 $opts->add( 'namespace', '', FormOptions::INTNULL );
150 $opts->add( 'invert', false );
151 $opts->add( 'associated', false );
152
153 return $opts;
154 }
155
156 /**
157 * Get custom show/hide filters
158 *
159 * @return array Map of filter URL param names to properties (msg/default)
160 */
161 protected function getCustomFilters() {
162 if ( $this->customFilters === null ) {
163 $this->customFilters = array();
164 Hooks::run( 'ChangesListSpecialPageFilters', array( $this, &$this->customFilters ) );
165 }
166
167 return $this->customFilters;
168 }
169
170 /**
171 * Fetch values for a FormOptions object from the WebRequest associated with this instance.
172 *
173 * Intended for subclassing, e.g. to add a backwards-compatibility layer.
174 *
175 * @param FormOptions $opts
176 * @return FormOptions
177 */
178 protected function fetchOptionsFromRequest( $opts ) {
179 $opts->fetchValuesFromRequest( $this->getRequest() );
180
181 return $opts;
182 }
183
184 /**
185 * Process $par and put options found in $opts. Used when including the page.
186 *
187 * @param string $par
188 * @param FormOptions $opts
189 */
190 public function parseParameters( $par, FormOptions $opts ) {
191 // nothing by default
192 }
193
194 /**
195 * Validate a FormOptions object generated by getDefaultOptions() with values already populated.
196 *
197 * @param FormOptions $opts
198 */
199 public function validateOptions( FormOptions $opts ) {
200 // nothing by default
201 }
202
203 /**
204 * Return an array of conditions depending of options set in $opts
205 *
206 * @param FormOptions $opts
207 * @return array
208 */
209 public function buildMainQueryConds( FormOptions $opts ) {
210 $dbr = $this->getDB();
211 $user = $this->getUser();
212 $conds = array();
213
214 // It makes no sense to hide both anons and logged-in users. When this occurs, try a guess on
215 // what the user meant and either show only bots or force anons to be shown.
216 $botsonly = false;
217 $hideanons = $opts['hideanons'];
218 if ( $opts['hideanons'] && $opts['hideliu'] ) {
219 if ( $opts['hidebots'] ) {
220 $hideanons = false;
221 } else {
222 $botsonly = true;
223 }
224 }
225
226 // Toggles
227 if ( $opts['hideminor'] ) {
228 $conds['rc_minor'] = 0;
229 }
230 if ( $opts['hidebots'] ) {
231 $conds['rc_bot'] = 0;
232 }
233 if ( $user->useRCPatrol() && $opts['hidepatrolled'] ) {
234 $conds['rc_patrolled'] = 0;
235 }
236 if ( $botsonly ) {
237 $conds['rc_bot'] = 1;
238 } else {
239 if ( $opts['hideliu'] ) {
240 $conds[] = 'rc_user = 0';
241 }
242 if ( $hideanons ) {
243 $conds[] = 'rc_user != 0';
244 }
245 }
246 if ( $opts['hidemyself'] ) {
247 if ( $user->getId() ) {
248 $conds[] = 'rc_user != ' . $dbr->addQuotes( $user->getId() );
249 } else {
250 $conds[] = 'rc_user_text != ' . $dbr->addQuotes( $user->getName() );
251 }
252 }
253 if ( $opts['hidecategorization'] === true ) {
254 $conds[] = 'rc_type != ' . $dbr->addQuotes( RC_CATEGORIZE );
255 }
256
257 // Namespace filtering
258 if ( $opts['namespace'] !== '' ) {
259 $selectedNS = $dbr->addQuotes( $opts['namespace'] );
260 $operator = $opts['invert'] ? '!=' : '=';
261 $boolean = $opts['invert'] ? 'AND' : 'OR';
262
263 // Namespace association (bug 2429)
264 if ( !$opts['associated'] ) {
265 $condition = "rc_namespace $operator $selectedNS";
266 } else {
267 // Also add the associated namespace
268 $associatedNS = $dbr->addQuotes(
269 MWNamespace::getAssociated( $opts['namespace'] )
270 );
271 $condition = "(rc_namespace $operator $selectedNS "
272 . $boolean
273 . " rc_namespace $operator $associatedNS)";
274 }
275
276 $conds[] = $condition;
277 }
278
279 return $conds;
280 }
281
282 /**
283 * Process the query
284 *
285 * @param array $conds
286 * @param FormOptions $opts
287 * @return bool|ResultWrapper Result or false
288 */
289 public function doMainQuery( $conds, $opts ) {
290 $tables = array( 'recentchanges' );
291 $fields = RecentChange::selectFields();
292 $query_options = array();
293 $join_conds = array();
294
295 ChangeTags::modifyDisplayQuery(
296 $tables,
297 $fields,
298 $conds,
299 $join_conds,
300 $query_options,
301 ''
302 );
303
304 if ( !$this->runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds,
305 $opts )
306 ) {
307 return false;
308 }
309
310 $dbr = $this->getDB();
311
312 return $dbr->select(
313 $tables,
314 $fields,
315 $conds,
316 __METHOD__,
317 $query_options,
318 $join_conds
319 );
320 }
321
322 protected function runMainQueryHook( &$tables, &$fields, &$conds,
323 &$query_options, &$join_conds, $opts
324 ) {
325 return Hooks::run(
326 'ChangesListSpecialPageQuery',
327 array( $this->getName(), &$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts )
328 );
329 }
330
331 /**
332 * Return a IDatabase object for reading
333 *
334 * @return IDatabase
335 */
336 protected function getDB() {
337 return wfGetDB( DB_SLAVE );
338 }
339
340 /**
341 * Send output to the OutputPage object, only called if not used feeds
342 *
343 * @param ResultWrapper $rows Database rows
344 * @param FormOptions $opts
345 */
346 public function webOutput( $rows, $opts ) {
347 if ( !$this->including() ) {
348 $this->outputFeedLinks();
349 $this->doHeader( $opts, $rows->numRows() );
350 }
351
352 $this->outputChangesList( $rows, $opts );
353 }
354
355 /**
356 * Output feed links.
357 */
358 public function outputFeedLinks() {
359 // nothing by default
360 }
361
362 /**
363 * Build and output the actual changes list.
364 *
365 * @param array $rows Database rows
366 * @param FormOptions $opts
367 */
368 abstract public function outputChangesList( $rows, $opts );
369
370 /**
371 * Set the text to be displayed above the changes
372 *
373 * @param FormOptions $opts
374 * @param int $numRows Number of rows in the result to show after this header
375 */
376 public function doHeader( $opts, $numRows ) {
377 $this->setTopText( $opts );
378
379 // @todo Lots of stuff should be done here.
380
381 $this->setBottomText( $opts );
382 }
383
384 /**
385 * Send the text to be displayed before the options. Should use $this->getOutput()->addWikiText()
386 * or similar methods to print the text.
387 *
388 * @param FormOptions $opts
389 */
390 function setTopText( FormOptions $opts ) {
391 // nothing by default
392 }
393
394 /**
395 * Send the text to be displayed after the options. Should use $this->getOutput()->addWikiText()
396 * or similar methods to print the text.
397 *
398 * @param FormOptions $opts
399 */
400 function setBottomText( FormOptions $opts ) {
401 // nothing by default
402 }
403
404 /**
405 * Get options to be displayed in a form
406 * @todo This should handle options returned by getDefaultOptions().
407 * @todo Not called by anything, should be called by something… doHeader() maybe?
408 *
409 * @param FormOptions $opts
410 * @return array
411 */
412 function getExtraOptions( $opts ) {
413 return array();
414 }
415
416 /**
417 * Return the legend displayed within the fieldset
418 * @todo This should not be static, then we can drop the parameter
419 * @todo Not called by anything, should be called by doHeader()
420 *
421 * @param IContextSource $context The object available as $this in non-static functions
422 * @return string
423 */
424 public static function makeLegend( IContextSource $context ) {
425 $user = $context->getUser();
426 # The legend showing what the letters and stuff mean
427 $legend = Html::openElement( 'dl' ) . "\n";
428 # Iterates through them and gets the messages for both letter and tooltip
429 $legendItems = $context->getConfig()->get( 'RecentChangesFlags' );
430 if ( !( $user->useRCPatrol() || $user->useNPPatrol() ) ) {
431 unset( $legendItems['unpatrolled'] );
432 }
433 foreach ( $legendItems as $key => $item ) { # generate items of the legend
434 $label = isset( $item['legend'] ) ? $item['legend'] : $item['title'];
435 $letter = $item['letter'];
436 $cssClass = isset( $item['class'] ) ? $item['class'] : $key;
437
438 $legend .= Html::element( 'dt',
439 array( 'class' => $cssClass ), $context->msg( $letter )->text()
440 ) . "\n" .
441 Html::rawElement( 'dd',
442 array( 'class' => Sanitizer::escapeClass( 'mw-changeslist-legend-' . $key ) ),
443 $context->msg( $label )->parse()
444 ) . "\n";
445 }
446 # (+-123)
447 $legend .= Html::rawElement( 'dt',
448 array( 'class' => 'mw-plusminus-pos' ),
449 $context->msg( 'recentchanges-legend-plusminus' )->parse()
450 ) . "\n";
451 $legend .= Html::element(
452 'dd',
453 array( 'class' => 'mw-changeslist-legend-plusminus' ),
454 $context->msg( 'recentchanges-label-plusminus' )->text()
455 ) . "\n";
456 $legend .= Html::closeElement( 'dl' ) . "\n";
457
458 # Collapsibility
459 $legend =
460 '<div class="mw-changeslist-legend">' .
461 $context->msg( 'recentchanges-legend-heading' )->parse() .
462 '<div class="mw-collapsible-content">' . $legend . '</div>' .
463 '</div>';
464
465 return $legend;
466 }
467
468 /**
469 * Add page-specific modules.
470 */
471 protected function addModules() {
472 $out = $this->getOutput();
473 // Styles and behavior for the legend box (see makeLegend())
474 $out->addModuleStyles( 'mediawiki.special.changeslist.legend' );
475 $out->addModules( 'mediawiki.special.changeslist.legend.js' );
476 }
477
478 protected function getGroupName() {
479 return 'changes';
480 }
481 }