[FileBackend] MultiWrite options to avoid pushing certain changes to all backends.
[lhc/web/wiklou.git] / includes / actions / InfoAction.php
1 <?php
2 /**
3 * Displays information about a page.
4 *
5 * Copyright © 2011 Alexandre Emsenhuber
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
20 *
21 * @file
22 * @ingroup Actions
23 */
24
25 class InfoAction extends FormlessAction {
26 /**
27 * Returns the name of the action this object responds to.
28 *
29 * @return string lowercase
30 */
31 public function getName() {
32 return 'info';
33 }
34
35 /**
36 * Whether this action can still be executed by a blocked user.
37 *
38 * @return bool
39 */
40 public function requiresUnblock() {
41 return false;
42 }
43
44 /**
45 * Whether this action requires the wiki not to be locked.
46 *
47 * @return bool
48 */
49 public function requiresWrite() {
50 return false;
51 }
52
53 /**
54 * Shows page information on GET request.
55 *
56 * @return string Page information that will be added to the output
57 */
58 public function onView() {
59 global $wgContLang, $wgDisableCounters, $wgRCMaxAge, $wgRestrictionTypes;
60
61 $user = $this->getUser();
62 $lang = $this->getLanguage();
63 $title = $this->getTitle();
64 $id = $title->getArticleID();
65
66 // Get page information that would be too "expensive" to retrieve by normal means
67 $userCanViewUnwatchedPages = $user->isAllowed( 'unwatchedpages' );
68 $pageInfo = self::pageCountInfo( $title, $userCanViewUnwatchedPages, $wgDisableCounters );
69
70 // Get page properties
71 $dbr = wfGetDB( DB_SLAVE );
72 $result = $dbr->select(
73 'page_props',
74 array( 'pp_propname', 'pp_value' ),
75 array( 'pp_page' => $id ),
76 __METHOD__
77 );
78
79 $pageProperties = array();
80 foreach ( $result as $row ) {
81 $pageProperties[$row->pp_propname] = $row->pp_value;
82 }
83
84 $content = '';
85 $table = '';
86
87 // Basic information
88 $content = $this->addHeader( $content, $this->msg( 'pageinfo-header-basic' )->text() );
89
90 // Display title
91 $displayTitle = $title->getPrefixedText();
92 if ( !empty( $pageProperties['displaytitle'] ) ) {
93 $displayTitle = $pageProperties['displaytitle'];
94 }
95
96 $table = $this->addRow( $table,
97 $this->msg( 'pageinfo-display-title' )->escaped(), $displayTitle );
98
99 // Default sort key
100 $sortKey = $title->getCategorySortKey();
101 if ( !empty( $pageProperties['defaultsort'] ) ) {
102 $sortKey = $pageProperties['defaultsort'];
103 }
104
105 $table = $this->addRow( $table,
106 $this->msg( 'pageinfo-default-sort' )->escaped(), $sortKey );
107
108 // Page length (in bytes)
109 $table = $this->addRow( $table,
110 $this->msg( 'pageinfo-length' )->escaped(), $lang->formatNum( $title->getLength() ) );
111
112 // Page ID (number not localised, as it's a database ID.)
113 $table = $this->addRow( $table,
114 $this->msg( 'pageinfo-article-id' )->escaped(), $id );
115
116 // Search engine status
117 $pOutput = new ParserOutput();
118 if ( isset( $pageProperties['noindex'] ) ) {
119 $pOutput->setIndexPolicy( 'noindex' );
120 }
121
122 // Use robot policy logic
123 $policy = $this->page->getRobotPolicy( 'view', $pOutput );
124 $table = $this->addRow( $table,
125 $this->msg( 'pageinfo-robot-policy' )->escaped(),
126 $this->msg( "pageinfo-robot-${policy['index']}" )->escaped()
127 );
128
129 if ( !$wgDisableCounters ) {
130 // Number of views
131 $table = $this->addRow( $table,
132 $this->msg( 'pageinfo-views' )->escaped(), $lang->formatNum( $pageInfo['views'] )
133 );
134 }
135
136 if ( $userCanViewUnwatchedPages ) {
137 // Number of page watchers
138 $table = $this->addRow( $table,
139 $this->msg( 'pageinfo-watchers' )->escaped(), $lang->formatNum( $pageInfo['watchers'] ) );
140 }
141
142 // Redirects to this page
143 $whatLinksHere = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedText() );
144 $table = $this->addRow( $table,
145 Linker::link(
146 $whatLinksHere,
147 $this->msg( 'pageinfo-redirects-name' )->escaped(),
148 array(),
149 array( 'hidelinks' => 1, 'hidetrans' => 1 )
150 ),
151 $this->msg( 'pageinfo-redirects-value' )
152 ->numParams( count( $title->getRedirectsHere() ) )->escaped()
153 );
154
155 // Subpages of this page
156 $prefixIndex = SpecialPage::getTitleFor( 'Prefixindex', $title->getPrefixedText() . '/' );
157 $table = $this->addRow( $table,
158 Linker::link( $prefixIndex, $this->msg( 'pageinfo-subpages-name' )->escaped() ),
159 $this->msg( 'pageinfo-subpages-value' )
160 ->numParams(
161 $pageInfo['subpages']['total'],
162 $pageInfo['subpages']['redirects'],
163 $pageInfo['subpages']['nonredirects'] )->escaped()
164 );
165
166 // Page protection
167 $content = $this->addTable( $content, $table );
168 $content = $this->addHeader( $content, $this->msg( 'pageinfo-header-restrictions' )->text() );
169 $table = '';
170
171 // Page protection
172 foreach ( $wgRestrictionTypes as $restrictionType ) {
173 $protectionLevel = implode( ', ', $title->getRestrictions( $restrictionType ) );
174 if ( $protectionLevel == '' ) {
175 // Allow all users
176 $message = $this->msg( 'protect-default' )->escaped();
177 } else {
178 // Administrators only
179 $message = $this->msg( "protect-level-$protectionLevel" );
180 if ( $message->isDisabled() ) {
181 // Require "$1" permission
182 $message = $this->msg( "protect-fallback", $protectionLevel )->parse();
183 } else {
184 $message = $message->escaped();
185 }
186 }
187
188 $table = $this->addRow( $table,
189 $this->msg( 'pageinfo-restriction', $restrictionType )->parse(), $message
190 );
191 }
192
193 // Edit history
194 $content = $this->addTable( $content, $table );
195 $content = $this->addHeader( $content, $this->msg( 'pageinfo-header-edits' )->text() );
196 $table = '';
197
198 // Page creator
199 $table = $this->addRow( $table,
200 $this->msg( 'pageinfo-firstuser' )->escaped(), $pageInfo['firstuser']
201 );
202
203 // Date of page creation
204 $table = $this->addRow( $table,
205 $this->msg( 'pageinfo-firsttime' )->escaped(), $lang->userTimeAndDate( $pageInfo['firsttime'], $user )
206 );
207
208 // Latest editor
209 $table = $this->addRow( $table,
210 $this->msg( 'pageinfo-lastuser' )->escaped(), $pageInfo['lastuser']
211 );
212
213 // Date of latest edit
214 $table = $this->addRow( $table,
215 $this->msg( 'pageinfo-lasttime' )->escaped(), $lang->userTimeAndDate( $pageInfo['lasttime'], $user )
216 );
217
218 // Total number of edits
219 $table = $this->addRow( $table,
220 $this->msg( 'pageinfo-edits' )->escaped(), $lang->formatNum( $pageInfo['edits'] )
221 );
222
223 // Total number of distinct authors
224 $table = $this->addRow( $table,
225 $this->msg( 'pageinfo-authors' )->escaped(), $lang->formatNum( $pageInfo['authors'] )
226 );
227
228 // Recent number of edits (within past 30 days)
229 $table = $this->addRow( $table,
230 $this->msg( 'pageinfo-recent-edits', $lang->formatDuration( $wgRCMaxAge ) )->escaped(),
231 $lang->formatNum( $pageInfo['recent_edits'] )
232 );
233
234 // Recent number of distinct authors
235 $table = $this->addRow( $table,
236 $this->msg( 'pageinfo-recent-authors' )->escaped(), $lang->formatNum( $pageInfo['recent_authors'] )
237 );
238
239 $content = $this->addTable( $content, $table );
240
241 // Array of MagicWord objects
242 $magicWords = MagicWord::getDoubleUnderscoreArray();
243
244 // Array of magic word IDs
245 $wordIDs = $magicWords->names;
246
247 // Array of IDs => localized magic words
248 $localizedWords = $wgContLang->getMagicWords();
249
250 $listItems = array();
251 foreach ( $pageProperties as $property => $value ) {
252 if ( in_array( $property, $wordIDs ) ) {
253 $listItems[] = Html::element( 'li', array(), $localizedWords[$property][1] );
254 }
255 }
256
257 $localizedList = Html::rawElement( 'ul', array(), implode( '', $listItems ) );
258 $hiddenCategories = $this->page->getHiddenCategories();
259 $transcludedTemplates = $title->getTemplateLinksFrom();
260
261 if ( count( $listItems ) > 0
262 || count( $hiddenCategories ) > 0
263 || count( $transcludedTemplates ) > 0 ) {
264 // Page properties
265 $content = $this->addHeader( $content, $this->msg( 'pageinfo-header-properties' )->text() );
266 $table = '';
267
268 // Magic words
269 if ( count( $listItems ) > 0 ) {
270 $table = $this->addRow( $table,
271 $this->msg( 'pageinfo-magic-words' )->numParams( count( $listItems ) )->escaped(),
272 $localizedList
273 );
274 }
275
276 // Hide "This page is a member of # hidden categories explanation
277 $content .= Html::element( 'style', array(),
278 '.mw-hiddenCategoriesExplanation { display: none; }' );
279
280 // Hidden categories
281 if ( count( $hiddenCategories ) > 0 ) {
282 $table = $this->addRow( $table,
283 $this->msg( 'pageinfo-hidden-categories' )
284 ->numParams( count( $hiddenCategories ) )->escaped(),
285 Linker::formatHiddenCategories( $hiddenCategories )
286 );
287 }
288
289 // Hide "Templates used on this page:" explanation
290 $content .= Html::element( 'style', array(),
291 '.mw-templatesUsedExplanation { display: none; }' );
292
293 // Transcluded templates
294 if ( count( $transcludedTemplates ) > 0 ) {
295 $table = $this->addRow( $table,
296 $this->msg( 'pageinfo-templates' )
297 ->numParams( count( $transcludedTemplates ) )->escaped(),
298 Linker::formatTemplates( $transcludedTemplates )
299 );
300 }
301
302 $content = $this->addTable( $content, $table );
303 }
304
305 return $content;
306 }
307
308 /**
309 * Returns page information that would be too "expensive" to retrieve by normal means.
310 *
311 * @param $title Title object
312 * @param $canViewUnwatched bool
313 * @param $disableCounter bool
314 * @return array
315 */
316 public static function pageCountInfo( $title, $canViewUnwatched, $disableCounter ) {
317 global $wgRCMaxAge;
318
319 wfProfileIn( __METHOD__ );
320 $id = $title->getArticleID();
321
322 $dbr = wfGetDB( DB_SLAVE );
323 $result = array();
324
325 if ( !$disableCounter ) {
326 // Number of views
327 $views = (int) $dbr->selectField(
328 'page',
329 'page_counter',
330 array( 'page_id' => $id ),
331 __METHOD__
332 );
333 $result['views'] = $views;
334 }
335
336 if ( $canViewUnwatched ) {
337 // Number of page watchers
338 $watchers = (int) $dbr->selectField(
339 'watchlist',
340 'COUNT(*)',
341 array(
342 'wl_namespace' => $title->getNamespace(),
343 'wl_title' => $title->getDBkey(),
344 ),
345 __METHOD__
346 );
347 $result['watchers'] = $watchers;
348 }
349
350 // Total number of edits
351 $edits = (int) $dbr->selectField(
352 'revision',
353 'COUNT(rev_page)',
354 array( 'rev_page' => $id ),
355 __METHOD__
356 );
357 $result['edits'] = $edits;
358
359 // Total number of distinct authors
360 $authors = (int) $dbr->selectField(
361 'revision',
362 'COUNT(DISTINCT rev_user_text)',
363 array( 'rev_page' => $id ),
364 __METHOD__
365 );
366 $result['authors'] = $authors;
367
368 // "Recent" threshold defined by $wgRCMaxAge
369 $threshold = $dbr->timestamp( time() - $wgRCMaxAge );
370
371 // Recent number of edits
372 $edits = (int) $dbr->selectField(
373 'revision',
374 'COUNT(rev_page)',
375 array(
376 'rev_page' => $id ,
377 "rev_timestamp >= $threshold"
378 ),
379 __METHOD__
380 );
381 $result['recent_edits'] = $edits;
382
383 // Recent number of distinct authors
384 $authors = (int) $dbr->selectField(
385 'revision',
386 'COUNT(DISTINCT rev_user_text)',
387 array(
388 'rev_page' => $id,
389 "rev_timestamp >= $threshold"
390 ),
391 __METHOD__
392 );
393 $result['recent_authors'] = $authors;
394
395 $conds = array( 'page_namespace' => $title->getNamespace(), 'page_is_redirect' => 1 );
396 $conds[] = 'page_title ' . $dbr->buildLike( $title->getDBkey() . '/', $dbr->anyString() );
397
398 // Subpages of this page (redirects)
399 $result['subpages']['redirects'] = (int) $dbr->selectField(
400 'page',
401 'COUNT(page_id)',
402 $conds,
403 __METHOD__ );
404
405 // Subpages of this page (non-redirects)
406 $conds['page_is_redirect'] = 0;
407 $result['subpages']['nonredirects'] = (int) $dbr->selectField(
408 'page',
409 'COUNT(page_id)',
410 $conds,
411 __METHOD__
412 );
413
414 // Subpages of this page (total)
415 $result['subpages']['total'] = $result['subpages']['redirects']
416 + $result['subpages']['nonredirects'];
417
418 // Latest editor + date of latest edit
419 $options = array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 1 );
420 $row = $dbr->fetchRow( $dbr->select(
421 'revision',
422 array( 'rev_user_text', 'rev_timestamp' ),
423 array( 'rev_page' => $id ),
424 __METHOD__,
425 $options
426 ) );
427
428 $result['firstuser'] = $row['rev_user_text'];
429 $result['firsttime'] = $row['rev_timestamp'];
430
431 // Latest editor + date of latest edit
432 $options['ORDER BY'] = 'rev_timestamp DESC';
433 $row = $dbr->fetchRow( $dbr->select(
434 'revision',
435 array( 'rev_user_text', 'rev_timestamp' ),
436 array( 'rev_page' => $id ),
437 __METHOD__,
438 $options
439 ) );
440
441 $result['lastuser'] = $row['rev_user_text'];
442 $result['lasttime'] = $row['rev_timestamp'];
443
444 wfProfileOut( __METHOD__ );
445 return $result;
446 }
447
448 /**
449 * Adds a header to the content that will be added to the output.
450 *
451 * @param $content string The content that will be added to the output
452 * @param $header string The value of the header
453 * @return string The content with the header added
454 */
455 protected function addHeader( $content, $header ) {
456 return $content . Html::element( 'h2', array(), $header );
457 }
458
459 /**
460 * Adds a row to a table that will be added to the content.
461 *
462 * @param $table string The table that will be added to the content
463 * @param $name string The name of the row
464 * @param $value string The value of the row
465 * @return string The table with the row added
466 */
467 protected function addRow( $table, $name, $value ) {
468 return $table . Html::rawElement( 'tr', array(),
469 Html::rawElement( 'td', array(), $name ) .
470 Html::rawElement( 'td', array(), $value )
471 );
472 }
473
474 /**
475 * Adds a table to the content that will be added to the output.
476 *
477 * @param $content string The content that will be added to the output
478 * @param $table string The table
479 * @return string The content with the table added
480 */
481 protected function addTable( $content, $table ) {
482 return $content . Html::rawElement( 'table', array( 'class' => 'wikitable mw-page-info' ),
483 $table );
484 }
485
486 /**
487 * Returns the description that goes below the <h1> tag.
488 *
489 * @return string
490 */
491 protected function getDescription() {
492 return '';
493 }
494
495 /**
496 * Returns the name that goes in the <h1> page title.
497 *
498 * @return string
499 */
500 protected function getPageTitle() {
501 return $this->msg( 'pageinfo-title', $this->getTitle()->getPrefixedText() )->text();
502 }
503 }