Fix mediawiki.ui.checkbox loading in mobile
[lhc/web/wiklou.git] / includes / logging / LogPage.php
1 <?php
2 /**
3 * Contain log classes
4 *
5 * Copyright © 2002, 2004 Brion Vibber <brion@pobox.com>
6 * https://www.mediawiki.org/
7 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License along
19 * with this program; if not, write to the Free Software Foundation, Inc.,
20 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21 * http://www.gnu.org/copyleft/gpl.html
22 *
23 * @file
24 */
25
26 /**
27 * Class to simplify the use of log pages.
28 * The logs are now kept in a table which is easier to manage and trim
29 * than ever-growing wiki pages.
30 *
31 */
32 class LogPage {
33 const DELETED_ACTION = 1;
34 const DELETED_COMMENT = 2;
35 const DELETED_USER = 4;
36 const DELETED_RESTRICTED = 8;
37
38 // Convenience fields
39 const SUPPRESSED_USER = 12;
40 const SUPPRESSED_ACTION = 9;
41
42 /** @var bool */
43 public $updateRecentChanges;
44
45 /** @var bool */
46 public $sendToUDP;
47
48 /** @var string Plaintext version of the message for IRC */
49 private $ircActionText;
50
51 /** @var string Plaintext version of the message */
52 private $actionText;
53
54 /** @var string One of '', 'block', 'protect', 'rights', 'delete',
55 * 'upload', 'move'
56 */
57 private $type;
58
59 /** @var string One of '', 'block', 'protect', 'rights', 'delete',
60 * 'upload', 'move', 'move_redir' */
61 private $action;
62
63 /** @var string Comment associated with action */
64 private $comment;
65
66 /** @var string Blob made of a parameters array */
67 private $params;
68
69 /** @var User The user doing the action */
70 private $doer;
71
72 /** @var Title */
73 private $target;
74
75 /**
76 * Constructor
77 *
78 * @param string $type One of '', 'block', 'protect', 'rights', 'delete',
79 * 'upload', 'move'
80 * @param bool $rc Whether to update recent changes as well as the logging table
81 * @param string $udp Pass 'UDP' to send to the UDP feed if NOT sent to RC
82 */
83 public function __construct( $type, $rc = true, $udp = 'skipUDP' ) {
84 $this->type = $type;
85 $this->updateRecentChanges = $rc;
86 $this->sendToUDP = ( $udp == 'UDP' );
87 }
88
89 /**
90 * @return int The log_id of the inserted log entry
91 */
92 protected function saveContent() {
93 global $wgLogRestrictions;
94
95 $dbw = wfGetDB( DB_MASTER );
96 $log_id = $dbw->nextSequenceValue( 'logging_log_id_seq' );
97
98 // @todo FIXME private/protected/public property?
99 $this->timestamp = $now = wfTimestampNow();
100 $data = array(
101 'log_id' => $log_id,
102 'log_type' => $this->type,
103 'log_action' => $this->action,
104 'log_timestamp' => $dbw->timestamp( $now ),
105 'log_user' => $this->doer->getId(),
106 'log_user_text' => $this->doer->getName(),
107 'log_namespace' => $this->target->getNamespace(),
108 'log_title' => $this->target->getDBkey(),
109 'log_page' => $this->target->getArticleID(),
110 'log_comment' => $this->comment,
111 'log_params' => $this->params
112 );
113 $dbw->insert( 'logging', $data, __METHOD__ );
114 $newId = !is_null( $log_id ) ? $log_id : $dbw->insertId();
115
116 # And update recentchanges
117 if ( $this->updateRecentChanges ) {
118 $titleObj = SpecialPage::getTitleFor( 'Log', $this->type );
119
120 RecentChange::notifyLog(
121 $now, $titleObj, $this->doer, $this->getRcComment(), '',
122 $this->type, $this->action, $this->target, $this->comment,
123 $this->params, $newId, $this->getRcCommentIRC()
124 );
125 } elseif ( $this->sendToUDP ) {
126 # Don't send private logs to UDP
127 if ( isset( $wgLogRestrictions[$this->type] ) && $wgLogRestrictions[$this->type] != '*' ) {
128 return $newId;
129 }
130
131 # Notify external application via UDP.
132 # We send this to IRC but do not want to add it the RC table.
133 $titleObj = SpecialPage::getTitleFor( 'Log', $this->type );
134 $rc = RecentChange::newLogEntry(
135 $now, $titleObj, $this->doer, $this->getRcComment(), '',
136 $this->type, $this->action, $this->target, $this->comment,
137 $this->params, $newId, $this->getRcCommentIRC()
138 );
139 $rc->notifyRCFeeds();
140 }
141
142 return $newId;
143 }
144
145 /**
146 * Get the RC comment from the last addEntry() call
147 *
148 * @return string
149 */
150 public function getRcComment() {
151 $rcComment = $this->actionText;
152
153 if ( $this->comment != '' ) {
154 if ( $rcComment == '' ) {
155 $rcComment = $this->comment;
156 } else {
157 $rcComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() .
158 $this->comment;
159 }
160 }
161
162 return $rcComment;
163 }
164
165 /**
166 * Get the RC comment from the last addEntry() call for IRC
167 *
168 * @return string
169 */
170 public function getRcCommentIRC() {
171 $rcComment = $this->ircActionText;
172
173 if ( $this->comment != '' ) {
174 if ( $rcComment == '' ) {
175 $rcComment = $this->comment;
176 } else {
177 $rcComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() .
178 $this->comment;
179 }
180 }
181
182 return $rcComment;
183 }
184
185 /**
186 * Get the comment from the last addEntry() call
187 */
188 public function getComment() {
189 return $this->comment;
190 }
191
192 /**
193 * Get the list of valid log types
194 *
195 * @return array Array of strings
196 */
197 public static function validTypes() {
198 global $wgLogTypes;
199
200 return $wgLogTypes;
201 }
202
203 /**
204 * Is $type a valid log type
205 *
206 * @param string $type Log type to check
207 * @return bool
208 */
209 public static function isLogType( $type ) {
210 return in_array( $type, LogPage::validTypes() );
211 }
212
213 /**
214 * Get the name for the given log type
215 *
216 * @param string $type Log type
217 * @return string Log name
218 * @deprecated since 1.19, warnings in 1.21. Use getName()
219 */
220 public static function logName( $type ) {
221 global $wgLogNames;
222
223 wfDeprecated( __METHOD__, '1.21' );
224
225 if ( isset( $wgLogNames[$type] ) ) {
226 return str_replace( '_', ' ', wfMessage( $wgLogNames[$type] )->text() );
227 } else {
228 // Bogus log types? Perhaps an extension was removed.
229 return $type;
230 }
231 }
232
233 /**
234 * Get the log header for the given log type
235 *
236 * @todo handle missing log types
237 * @param string $type Logtype
238 * @return string Header text of this logtype
239 * @deprecated since 1.19, warnings in 1.21. Use getDescription()
240 */
241 public static function logHeader( $type ) {
242 global $wgLogHeaders;
243
244 wfDeprecated( __METHOD__, '1.21' );
245
246 return wfMessage( $wgLogHeaders[$type] )->parse();
247 }
248
249 /**
250 * Generate text for a log entry.
251 * Only LogFormatter should call this function.
252 *
253 * @param string $type Log type
254 * @param string $action Log action
255 * @param Title|null $title Title object or null
256 * @param Skin|null $skin Skin object or null. If null, we want to use the wiki
257 * content language, since that will go to the IRC feed.
258 * @param array $params Parameters
259 * @param bool $filterWikilinks Whether to filter wiki links
260 * @return string HTML
261 */
262 public static function actionText( $type, $action, $title = null, $skin = null,
263 $params = array(), $filterWikilinks = false
264 ) {
265 global $wgLang, $wgContLang, $wgLogActions;
266
267 if ( is_null( $skin ) ) {
268 $langObj = $wgContLang;
269 $langObjOrNull = null;
270 } else {
271 $langObj = $wgLang;
272 $langObjOrNull = $wgLang;
273 }
274
275 $key = "$type/$action";
276
277 if ( isset( $wgLogActions[$key] ) ) {
278 if ( is_null( $title ) ) {
279 $rv = wfMessage( $wgLogActions[$key] )->inLanguage( $langObj )->escaped();
280 } else {
281 $titleLink = self::getTitleLink( $type, $langObjOrNull, $title, $params );
282
283 if ( count( $params ) == 0 ) {
284 $rv = wfMessage( $wgLogActions[$key] )->rawParams( $titleLink )
285 ->inLanguage( $langObj )->escaped();
286 } else {
287 $details = '';
288 array_unshift( $params, $titleLink );
289
290 // User suppression
291 if ( preg_match( '/^(block|suppress)\/(block|reblock)$/', $key ) ) {
292 if ( $skin ) {
293 // Localize the duration, and add a tooltip
294 // in English to help visitors from other wikis.
295 // The lrm is needed to make sure that the number
296 // is shown on the correct side of the tooltip text.
297 $durationTooltip = '&lrm;' . htmlspecialchars( $params[1] );
298 $params[1] = "<span class='blockExpiry' title='$durationTooltip'>" .
299 $wgLang->translateBlockExpiry( $params[1] ) . '</span>';
300 } else {
301 $params[1] = $wgContLang->translateBlockExpiry( $params[1] );
302 }
303
304 $params[2] = isset( $params[2] ) ?
305 self::formatBlockFlags( $params[2], $langObj ) : '';
306 // Page protections
307 } elseif ( $type == 'protect' && count( $params ) == 3 ) {
308 // Restrictions and expiries
309 if ( $skin ) {
310 $details .= $wgLang->getDirMark() . htmlspecialchars( " {$params[1]}" );
311 } else {
312 $details .= " {$params[1]}";
313 }
314
315 // Cascading flag...
316 if ( $params[2] ) {
317 $text = wfMessage( 'protect-summary-cascade' )
318 ->inLanguage( $langObj )->text();
319 $details .= ' ';
320 $details .= wfMessage( 'brackets', $text )->inLanguage( $langObj )->text();
321
322 }
323 }
324
325 $rv = wfMessage( $wgLogActions[$key] )->rawParams( $params )
326 ->inLanguage( $langObj )->escaped() . $details;
327 }
328 }
329 } else {
330 global $wgLogActionsHandlers;
331
332 if ( isset( $wgLogActionsHandlers[$key] ) ) {
333 $args = func_get_args();
334 $rv = call_user_func_array( $wgLogActionsHandlers[$key], $args );
335 } else {
336 wfDebug( "LogPage::actionText - unknown action $key\n" );
337 $rv = "$action";
338 }
339 }
340
341 // For the perplexed, this feature was added in r7855 by Erik.
342 // The feature was added because we liked adding [[$1]] in our log entries
343 // but the log entries are parsed as Wikitext on RecentChanges but as HTML
344 // on Special:Log. The hack is essentially that [[$1]] represented a link
345 // to the title in question. The first parameter to the HTML version (Special:Log)
346 // is that link in HTML form, and so this just gets rid of the ugly [[]].
347 // However, this is a horrible hack and it doesn't work like you expect if, say,
348 // you want to link to something OTHER than the title of the log entry.
349 // The real problem, which Erik was trying to fix (and it sort-of works now) is
350 // that the same messages are being treated as both wikitext *and* HTML.
351 if ( $filterWikilinks ) {
352 $rv = str_replace( '[[', '', $rv );
353 $rv = str_replace( ']]', '', $rv );
354 }
355
356 return $rv;
357 }
358
359 /**
360 * @todo Document
361 * @param string $type
362 * @param Language|null $lang
363 * @param Title $title
364 * @param array $params
365 * @return string
366 */
367 protected static function getTitleLink( $type, $lang, $title, &$params ) {
368 if ( !$lang ) {
369 return $title->getPrefixedText();
370 }
371
372 switch ( $type ) {
373 case 'move':
374 $titleLink = Linker::link(
375 $title,
376 htmlspecialchars( $title->getPrefixedText() ),
377 array(),
378 array( 'redirect' => 'no' )
379 );
380
381 $targetTitle = Title::newFromText( $params[0] );
382
383 if ( !$targetTitle ) {
384 # Workaround for broken database
385 $params[0] = htmlspecialchars( $params[0] );
386 } else {
387 $params[0] = Linker::link(
388 $targetTitle,
389 htmlspecialchars( $params[0] )
390 );
391 }
392 break;
393 case 'block':
394 if ( substr( $title->getText(), 0, 1 ) == '#' ) {
395 $titleLink = $title->getText();
396 } else {
397 // @todo Store the user identifier in the parameters
398 // to make this faster for future log entries
399 $id = User::idFromName( $title->getText() );
400 $titleLink = Linker::userLink( $id, $title->getText() )
401 . Linker::userToolLinks( $id, $title->getText(), false, Linker::TOOL_LINKS_NOBLOCK );
402 }
403 break;
404 case 'merge':
405 $titleLink = Linker::link(
406 $title,
407 $title->getPrefixedText(),
408 array(),
409 array( 'redirect' => 'no' )
410 );
411 $params[0] = Linker::link(
412 Title::newFromText( $params[0] ),
413 htmlspecialchars( $params[0] )
414 );
415 $params[1] = $lang->timeanddate( $params[1] );
416 break;
417 default:
418 if ( $title->isSpecialPage() ) {
419 list( $name, $par ) = SpecialPageFactory::resolveAlias( $title->getDBkey() );
420
421 # Use the language name for log titles, rather than Log/X
422 if ( $name == 'Log' ) {
423 $logPage = new LogPage( $par );
424 $titleLink = Linker::link( $title, $logPage->getName()->escaped() );
425 $titleLink = wfMessage( 'parentheses' )
426 ->inLanguage( $lang )
427 ->rawParams( $titleLink )
428 ->escaped();
429 } else {
430 $titleLink = Linker::link( $title );
431 }
432 } else {
433 $titleLink = Linker::link( $title );
434 }
435 }
436
437 return $titleLink;
438 }
439
440 /**
441 * Add a log entry
442 *
443 * @param string $action One of '', 'block', 'protect', 'rights', 'delete',
444 * 'upload', 'move', 'move_redir'
445 * @param Title $target Title object
446 * @param string $comment Description associated
447 * @param array $params Parameters passed later to wfMessage function
448 * @param null|int|User $doer The user doing the action. null for $wgUser
449 *
450 * @return int The log_id of the inserted log entry
451 */
452 public function addEntry( $action, $target, $comment, $params = array(), $doer = null ) {
453 global $wgContLang;
454
455 if ( !is_array( $params ) ) {
456 $params = array( $params );
457 }
458
459 if ( $comment === null ) {
460 $comment = '';
461 }
462
463 # Trim spaces on user supplied text
464 $comment = trim( $comment );
465
466 # Truncate for whole multibyte characters.
467 $comment = $wgContLang->truncate( $comment, 255 );
468
469 $this->action = $action;
470 $this->target = $target;
471 $this->comment = $comment;
472 $this->params = LogPage::makeParamBlob( $params );
473
474 if ( $doer === null ) {
475 global $wgUser;
476 $doer = $wgUser;
477 } elseif ( !is_object( $doer ) ) {
478 $doer = User::newFromId( $doer );
479 }
480
481 $this->doer = $doer;
482
483 $logEntry = new ManualLogEntry( $this->type, $action );
484 $logEntry->setTarget( $target );
485 $logEntry->setPerformer( $doer );
486 $logEntry->setParameters( $params );
487
488 $formatter = LogFormatter::newFromEntry( $logEntry );
489 $context = RequestContext::newExtraneousContext( $target );
490 $formatter->setContext( $context );
491
492 $this->actionText = $formatter->getPlainActionText();
493 $this->ircActionText = $formatter->getIRCActionText();
494
495 return $this->saveContent();
496 }
497
498 /**
499 * Add relations to log_search table
500 *
501 * @param string $field
502 * @param array $values
503 * @param int $logid
504 * @return bool
505 */
506 public function addRelations( $field, $values, $logid ) {
507 if ( !strlen( $field ) || empty( $values ) ) {
508 return false; // nothing
509 }
510
511 $data = array();
512
513 foreach ( $values as $value ) {
514 $data[] = array(
515 'ls_field' => $field,
516 'ls_value' => $value,
517 'ls_log_id' => $logid
518 );
519 }
520
521 $dbw = wfGetDB( DB_MASTER );
522 $dbw->insert( 'log_search', $data, __METHOD__, 'IGNORE' );
523
524 return true;
525 }
526
527 /**
528 * Create a blob from a parameter array
529 *
530 * @param array $params
531 * @return string
532 */
533 public static function makeParamBlob( $params ) {
534 return implode( "\n", $params );
535 }
536
537 /**
538 * Extract a parameter array from a blob
539 *
540 * @param string $blob
541 * @return array
542 */
543 public static function extractParams( $blob ) {
544 if ( $blob === '' ) {
545 return array();
546 } else {
547 return explode( "\n", $blob );
548 }
549 }
550
551 /**
552 * Convert a comma-delimited list of block log flags
553 * into a more readable (and translated) form
554 *
555 * @param string $flags Flags to format
556 * @param Language $lang
557 * @return string
558 */
559 public static function formatBlockFlags( $flags, $lang ) {
560 $flags = trim( $flags );
561 if ( $flags === '' ) {
562 return ''; //nothing to do
563 }
564 $flags = explode( ',', $flags );
565 $flagsCount = count( $flags );
566
567 for ( $i = 0; $i < $flagsCount; $i++ ) {
568 $flags[$i] = self::formatBlockFlag( $flags[$i], $lang );
569 }
570
571 return wfMessage( 'parentheses' )->inLanguage( $lang )
572 ->rawParams( $lang->commaList( $flags ) )->escaped();
573 }
574
575 /**
576 * Translate a block log flag if possible
577 *
578 * @param int $flag Flag to translate
579 * @param Language $lang Language object to use
580 * @return string
581 */
582 public static function formatBlockFlag( $flag, $lang ) {
583 static $messages = array();
584
585 if ( !isset( $messages[$flag] ) ) {
586 $messages[$flag] = htmlspecialchars( $flag ); // Fallback
587
588 // For grepping. The following core messages can be used here:
589 // * block-log-flags-angry-autoblock
590 // * block-log-flags-anononly
591 // * block-log-flags-hiddenname
592 // * block-log-flags-noautoblock
593 // * block-log-flags-nocreate
594 // * block-log-flags-noemail
595 // * block-log-flags-nousertalk
596 $msg = wfMessage( 'block-log-flags-' . $flag )->inLanguage( $lang );
597
598 if ( $msg->exists() ) {
599 $messages[$flag] = $msg->escaped();
600 }
601 }
602
603 return $messages[$flag];
604 }
605
606 /**
607 * Name of the log.
608 * @return Message
609 * @since 1.19
610 */
611 public function getName() {
612 global $wgLogNames;
613
614 // BC
615 if ( isset( $wgLogNames[$this->type] ) ) {
616 $key = $wgLogNames[$this->type];
617 } else {
618 $key = 'log-name-' . $this->type;
619 }
620
621 return wfMessage( $key );
622 }
623
624 /**
625 * Description of this log type.
626 * @return Message
627 * @since 1.19
628 */
629 public function getDescription() {
630 global $wgLogHeaders;
631 // BC
632 if ( isset( $wgLogHeaders[$this->type] ) ) {
633 $key = $wgLogHeaders[$this->type];
634 } else {
635 $key = 'log-description-' . $this->type;
636 }
637
638 return wfMessage( $key );
639 }
640
641 /**
642 * Returns the right needed to read this log type.
643 * @return string
644 * @since 1.19
645 */
646 public function getRestriction() {
647 global $wgLogRestrictions;
648 if ( isset( $wgLogRestrictions[$this->type] ) ) {
649 $restriction = $wgLogRestrictions[$this->type];
650 } else {
651 // '' always returns true with $user->isAllowed()
652 $restriction = '';
653 }
654
655 return $restriction;
656 }
657
658 /**
659 * Tells if this log is not viewable by all.
660 * @return bool
661 * @since 1.19
662 */
663 public function isRestricted() {
664 $restriction = $this->getRestriction();
665
666 return $restriction !== '' && $restriction !== '*';
667 }
668 }