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