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