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