Merge "Improve OutputPage::showErrorPage method documentation"
[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 $rcSubpage, $rcOptions; // @todo Rename these, make protected
32 protected $customFilters;
33
34 /**
35 * The feed format to output as (either 'rss' or 'atom'), or null if no
36 * feed output was requested
37 *
38 * @var string $feedFormat
39 */
40 protected $feedFormat;
41
42 /**
43 * Main execution point
44 *
45 * @param string $subpage
46 */
47 public function execute( $subpage ) {
48 $this->rcSubpage = $subpage;
49 $this->feedFormat = $this->including() ? null : $this->getRequest()->getVal( 'feed' );
50 if ( $this->feedFormat !== 'atom' && $this->feedFormat !== 'rss' ) {
51 $this->feedFormat = null;
52 }
53
54 $this->setHeaders();
55 $this->outputHeader();
56 $this->addModules();
57
58 $opts = $this->getOptions();
59 // Fetch results, prepare a batch link existence check query
60 $conds = $this->buildMainQueryConds( $opts );
61 $rows = $this->doMainQuery( $conds, $opts );
62 if ( $rows === false ) {
63 if ( !$this->including() ) {
64 $this->doHeader( $opts );
65 }
66
67 return;
68 }
69
70 if ( !$this->feedFormat ) {
71 $batch = new LinkBatch;
72 foreach ( $rows as $row ) {
73 $batch->add( NS_USER, $row->rc_user_text );
74 $batch->add( NS_USER_TALK, $row->rc_user_text );
75 $batch->add( $row->rc_namespace, $row->rc_title );
76 }
77 $batch->execute();
78 }
79 if ( $this->feedFormat ) {
80 list( $changesFeed, $formatter ) = $this->getFeedObject( $this->feedFormat );
81 /** @var ChangesFeed $changesFeed */
82 $changesFeed->execute( $formatter, $rows, $this->checkLastModified( $this->feedFormat ), $opts );
83 } else {
84 $this->webOutput( $rows, $opts );
85 }
86
87 $rows->free();
88 }
89
90 /**
91 * Get the current FormOptions for this request
92 *
93 * @return FormOptions
94 */
95 public function getOptions() {
96 if ( $this->rcOptions === null ) {
97 $this->rcOptions = $this->setup( $this->rcSubpage );
98 }
99
100 return $this->rcOptions;
101 }
102
103 /**
104 * Create a FormOptions object with options as specified by the user
105 *
106 * @param array $parameters
107 *
108 * @return FormOptions
109 */
110 public function setup( $parameters ) {
111 $opts = $this->getDefaultOptions();
112 foreach ( $this->getCustomFilters() as $key => $params ) {
113 $opts->add( $key, $params['default'] );
114 }
115
116 $opts = $this->fetchOptionsFromRequest( $opts );
117
118 // Give precedence to subpage syntax
119 if ( $parameters !== null ) {
120 $this->parseParameters( $parameters, $opts );
121 }
122
123 $this->validateOptions( $opts );
124
125 return $opts;
126 }
127
128 /**
129 * Get a FormOptions object containing the default options. By default returns some basic options,
130 * you might want to not call parent method and discard them, or to override default values.
131 *
132 * @return FormOptions
133 */
134 public function getDefaultOptions() {
135 $opts = new FormOptions();
136
137 $opts->add( 'hideminor', false );
138 $opts->add( 'hidebots', false );
139 $opts->add( 'hideanons', false );
140 $opts->add( 'hideliu', false );
141 $opts->add( 'hidepatrolled', false );
142 $opts->add( 'hidemyself', false );
143
144 $opts->add( 'namespace', '', FormOptions::INTNULL );
145 $opts->add( 'invert', false );
146 $opts->add( 'associated', false );
147
148 return $opts;
149 }
150
151 /**
152 * Get custom show/hide filters
153 *
154 * @return array Map of filter URL param names to properties (msg/default)
155 */
156 protected function getCustomFilters() {
157 // @todo Fire a Special{$this->getName()}Filters hook here
158 return array();
159 }
160
161 /**
162 * Fetch values for a FormOptions object from the WebRequest associated with this instance.
163 *
164 * Intended for subclassing, e.g. to add a backwards-compatibility layer.
165 *
166 * @param FormOptions $parameters
167 * @return FormOptions
168 */
169 protected function fetchOptionsFromRequest( $opts ) {
170 $opts->fetchValuesFromRequest( $this->getRequest() );
171 return $opts;
172 }
173
174 /**
175 * Process $par and put options found in $opts. Used when including the page.
176 *
177 * @param string $par
178 * @param FormOptions $opts
179 */
180 public function parseParameters( $par, FormOptions $opts ) {
181 // nothing by default
182 }
183
184 /**
185 * Validate a FormOptions object generated by getDefaultOptions() with values already populated.
186 *
187 * @param FormOptions $opts
188 */
189 public function validateOptions( FormOptions $opts ) {
190 // nothing by default
191 }
192
193 /**
194 * Return an array of conditions depending of options set in $opts
195 * @todo Whyyyy is this mutating $opts…
196 *
197 * @param FormOptions $opts
198 * @return array
199 */
200 public function buildMainQueryConds( FormOptions $opts ) {
201 $dbr = $this->getDB();
202 $user = $this->getUser();
203 $conds = array();
204
205 // It makes no sense to hide both anons and logged-in users
206 // Where this occurs, force anons to be shown
207 $botsOnly = false;
208 if ( $opts['hideanons'] && $opts['hideliu'] ) {
209 // Check if the user wants to show bots only
210 if ( $opts['hidebots'] ) {
211 $opts['hideanons'] = false;
212 } else {
213 $botsOnly = true;
214 }
215 }
216
217 // Toggles
218 if ( $opts['hideminor'] ) {
219 $conds['rc_minor'] = 0;
220 }
221 if ( $opts['hidebots'] ) {
222 $conds['rc_bot'] = 0;
223 }
224 if ( $user->useRCPatrol() && $opts['hidepatrolled'] ) {
225 $conds['rc_patrolled'] = 0;
226 }
227 if ( $botsOnly ) {
228 $conds['rc_bot'] = 1;
229 } else {
230 if ( $opts['hideliu'] ) {
231 $conds[] = 'rc_user = 0';
232 }
233 if ( $opts['hideanons'] ) {
234 $conds[] = 'rc_user != 0';
235 }
236 }
237 if ( $opts['hidemyself'] ) {
238 if ( $user->getId() ) {
239 $conds[] = 'rc_user != ' . $dbr->addQuotes( $user->getId() );
240 } else {
241 $conds[] = 'rc_user_text != ' . $dbr->addQuotes( $user->getName() );
242 }
243 }
244
245 // Namespace filtering
246 if ( $opts['namespace'] !== '' ) {
247 $selectedNS = $dbr->addQuotes( $opts['namespace'] );
248 $operator = $opts['invert'] ? '!=' : '=';
249 $boolean = $opts['invert'] ? 'AND' : 'OR';
250
251 // Namespace association (bug 2429)
252 if ( !$opts['associated'] ) {
253 $condition = "rc_namespace $operator $selectedNS";
254 } else {
255 // Also add the associated namespace
256 $associatedNS = $dbr->addQuotes(
257 MWNamespace::getAssociated( $opts['namespace'] )
258 );
259 $condition = "(rc_namespace $operator $selectedNS "
260 . $boolean
261 . " rc_namespace $operator $associatedNS)";
262 }
263
264 $conds[] = $condition;
265 }
266
267 return $conds;
268 }
269
270 /**
271 * Process the query
272 * @todo This should build some basic processing here…
273 *
274 * @param array $conds
275 * @param FormOptions $opts
276 * @return bool|ResultWrapper Result or false (for Recentchangeslinked only)
277 */
278 abstract public function doMainQuery( $conds, $opts );
279
280 /**
281 * Return a DatabaseBase object for reading
282 *
283 * @return DatabaseBase
284 */
285 protected function getDB() {
286 return wfGetDB( DB_SLAVE );
287 }
288
289 /**
290 * Send output to the OutputPage object, only called if not used feeds
291 * @todo This should do most, if not all, of the outputting now done by subclasses
292 *
293 * @param ResultWrapper $rows Database rows
294 * @param FormOptions $opts
295 */
296 abstract public function webOutput( $rows, $opts );
297
298 /**
299 * Return the text to be displayed above the changes
300 * @todo Not called by anything, should be called by webOutput()
301 *
302 * @param FormOptions $opts
303 * @return string XHTML
304 */
305 public function doHeader( $opts ) {
306 $this->setTopText( $opts );
307
308 // @todo Lots of stuff should be done here.
309
310 $this->setBottomText( $opts );
311 }
312
313 /**
314 * Send the text to be displayed before the options. Should use $this->getOutput()->addWikiText()
315 * or similar methods to print the text.
316 *
317 * @param FormOptions $opts
318 */
319 function setTopText( FormOptions $opts ) {
320 // nothing by default
321 }
322
323 /**
324 * Send the text to be displayed after the options. Should use $this->getOutput()->addWikiText()
325 * or similar methods to print the text.
326 *
327 * @param FormOptions $opts
328 */
329 function setBottomText( FormOptions $opts ) {
330 // nothing by default
331 }
332
333 /**
334 * Get options to be displayed in a form
335 * @todo This should handle options returned by getDefaultOptions().
336 * @todo Not called by anything, should be called by something… doHeader() maybe?
337 *
338 * @param FormOptions $opts
339 * @return array
340 */
341 function getExtraOptions( $opts ) {
342 return array();
343 }
344
345 /**
346 * Return the legend displayed within the fieldset
347 * @todo This should not be static, then we can drop the parameter
348 * @todo Not called by anything, should be called by doHeader()
349 *
350 * @param $context the object available as $this in non-static functions
351 * @return string
352 */
353 public static function makeLegend( IContextSource $context ) {
354 global $wgRecentChangesFlags;
355 $user = $context->getUser();
356 # The legend showing what the letters and stuff mean
357 $legend = Xml::openElement( 'dl' ) . "\n";
358 # Iterates through them and gets the messages for both letter and tooltip
359 $legendItems = $wgRecentChangesFlags;
360 if ( !$user->useRCPatrol() ) {
361 unset( $legendItems['unpatrolled'] );
362 }
363 foreach ( $legendItems as $key => $legendInfo ) { # generate items of the legend
364 $label = $legendInfo['title'];
365 $letter = $legendInfo['letter'];
366 $cssClass = isset( $legendInfo['class'] ) ? $legendInfo['class'] : $key;
367
368 $legend .= Xml::element( 'dt',
369 array( 'class' => $cssClass ), $context->msg( $letter )->text()
370 ) . "\n";
371 if ( $key === 'newpage' ) {
372 $legend .= Xml::openElement( 'dd' );
373 $legend .= $context->msg( $label )->escaped();
374 $legend .= ' ' . $context->msg( 'recentchanges-legend-newpage' )->parse();
375 $legend .= Xml::closeElement( 'dd' ) . "\n";
376 } else {
377 $legend .= Xml::element( 'dd', array(),
378 $context->msg( $label )->text()
379 ) . "\n";
380 }
381 }
382 # (+-123)
383 $legend .= Xml::tags( 'dt',
384 array( 'class' => 'mw-plusminus-pos' ),
385 $context->msg( 'recentchanges-legend-plusminus' )->parse()
386 ) . "\n";
387 $legend .= Xml::element(
388 'dd',
389 array( 'class' => 'mw-changeslist-legend-plusminus' ),
390 $context->msg( 'recentchanges-label-plusminus' )->text()
391 ) . "\n";
392 $legend .= Xml::closeElement( 'dl' ) . "\n";
393
394 # Collapsibility
395 $legend =
396 '<div class="mw-changeslist-legend">' .
397 $context->msg( 'recentchanges-legend-heading' )->parse() .
398 '<div class="mw-collapsible-content">' . $legend . '</div>' .
399 '</div>';
400
401 return $legend;
402 }
403
404 /**
405 * Add page-specific modules.
406 */
407 protected function addModules() {
408 $out = $this->getOutput();
409 // Styles and behavior for the legend box (see makeLegend())
410 $out->addModuleStyles( 'mediawiki.special.changeslist.legend' );
411 $out->addModules( 'mediawiki.special.changeslist.legend.js' );
412 }
413
414 /**
415 * Return an array with a ChangesFeed object and ChannelFeed object.
416 *
417 * This is intentionally not abstract not to require subclasses which don't
418 * use feeds functionality to implement it.
419 *
420 * @param string $feedFormat Feed's format (either 'rss' or 'atom')
421 * @return array
422 */
423 public function getFeedObject( $feedFormat ) {
424 throw new MWException( "Not implemented" );
425 }
426
427 /**
428 * Get last-modified date, for client caching. Not implemented by default
429 * (returns current time).
430 *
431 * @param string $feedFormat
432 * @return string|bool
433 */
434 public function checkLastModified( $feedFormat ) {
435 return wfTimestampNow();
436 }
437
438 protected function getGroupName() {
439 return 'changes';
440 }
441 }