Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
68.06% covered (warning)
68.06%
98 / 144
43.75% covered (danger)
43.75%
7 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
LogPage
68.53% covered (warning)
68.53%
98 / 143
43.75% covered (danger)
43.75%
7 / 16
76.39
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 saveContent
63.83% covered (warning)
63.83%
30 / 47
0.00% covered (danger)
0.00%
0 / 1
6.18
 getRcComment
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 getRcCommentIRC
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 getComment
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 validTypes
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 isLogType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 actionText
72.00% covered (warning)
72.00%
18 / 25
0.00% covered (danger)
0.00%
0 / 1
6.79
 getTitleLink
15.38% covered (danger)
15.38%
2 / 13
0.00% covered (danger)
0.00%
0 / 1
13.69
 addEntry
90.48% covered (success)
90.48%
19 / 21
0.00% covered (danger)
0.00%
0 / 1
3.01
 makeParamBlob
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 extractParams
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getName
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getDescription
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getRestriction
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isRestricted
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Contain log classes
4 *
5 * Copyright © 2002, 2004 Brooke Vibber <bvibber@wikimedia.org>
6 * https://www.mediawiki.org/
7 *
8 * @license GPL-2.0-or-later
9 * @file
10 */
11
12namespace MediaWiki\Logging;
13
14use MediaWiki\Context\RequestContext;
15use MediaWiki\Language\Language;
16use MediaWiki\MainConfigNames;
17use MediaWiki\MediaWikiServices;
18use MediaWiki\Message\Message;
19use MediaWiki\Skin\Skin;
20use MediaWiki\SpecialPage\SpecialPage;
21use MediaWiki\Title\Title;
22use MediaWiki\User\User;
23use MediaWiki\User\UserIdentity;
24
25/**
26 * Class to simplify the use of log pages.
27 * The logs are now kept in a table which is easier to manage and trim
28 * than ever-growing wiki pages.
29 *
30 * @newable
31 * @note marked as newable in 1.35 for lack of a better alternative,
32 *       but should become a stateless service, use the command pattern.
33 */
34class LogPage {
35    public const DELETED_ACTION = 1;
36    public const DELETED_COMMENT = 2;
37    public const DELETED_USER = 4;
38    public const DELETED_RESTRICTED = 8;
39
40    // Convenience fields
41    public const SUPPRESSED_USER = self::DELETED_USER | self::DELETED_RESTRICTED;
42    public const SUPPRESSED_ACTION = self::DELETED_ACTION | self::DELETED_RESTRICTED;
43
44    /** @var bool */
45    public $updateRecentChanges;
46
47    /** @var bool */
48    public $sendToUDP;
49
50    /** @var string Plaintext version of the message for IRC */
51    private $ircActionText;
52
53    /** @var string Plaintext version of the message */
54    private $actionText;
55
56    /** @var string One of '', 'block', 'protect', 'rights', 'delete',
57     *    'upload', 'move'
58     */
59    private $type;
60
61    /** @var string One of '', 'block', 'protect', 'rights', 'delete',
62     *   'upload', 'move', 'move_redir'
63     */
64    private $action;
65
66    /** @var string Comment associated with action */
67    private $comment;
68
69    /** @var string Blob made of a parameters array */
70    private $params;
71
72    /** @var UserIdentity The user doing the action */
73    private $performer;
74
75    /** @var Title */
76    private $target;
77
78    /**
79     * @stable to call
80     * @param string $type One of '', 'block', 'protect', 'rights', 'delete',
81     *   'upload', 'move'
82     * @param bool $rc Whether to update recent changes as well as the logging table
83     * @param string $udp Pass 'UDP' to send to the UDP feed if NOT sent to RC
84     */
85    public function __construct( $type, $rc = true, $udp = 'skipUDP' ) {
86        $this->type = $type;
87        $this->updateRecentChanges = $rc;
88        $this->sendToUDP = ( $udp == 'UDP' );
89    }
90
91    /**
92     * @return int The log_id of the inserted log entry
93     */
94    protected function saveContent() {
95        $services = MediaWikiServices::getInstance();
96        $logRestrictions = $services->getMainConfig()->get( MainConfigNames::LogRestrictions );
97        $recentChangeStore = $services->getRecentChangeStore();
98        $recentChangeRCFeedNotifier = $services->getRecentChangeRCFeedNotifier();
99        $dbw = $services->getConnectionProvider()->getPrimaryDatabase();
100
101        $now = wfTimestampNow();
102        $actorId = $services->getActorNormalization()
103            ->acquireActorId( $this->performer, $dbw );
104        $data = [
105            'log_type' => $this->type,
106            'log_action' => $this->action,
107            'log_timestamp' => $dbw->timestamp( $now ),
108            'log_actor' => $actorId,
109            'log_namespace' => $this->target->getNamespace(),
110            'log_title' => $this->target->getDBkey(),
111            'log_page' => $this->target->getArticleID(),
112            'log_params' => $this->params
113        ];
114        $data += $services->getCommentStore()->insert(
115            $dbw,
116            'log_comment',
117            $this->comment
118        );
119        $dbw->newInsertQueryBuilder()
120            ->insertInto( 'logging' )
121            ->row( $data )
122            ->caller( __METHOD__ )->execute();
123        $newId = $dbw->insertId();
124
125        // Don't add private logs to RC or send them to UDP
126        if ( isset( $logRestrictions[$this->type] ) && $logRestrictions[$this->type] != '*' ) {
127            return $newId;
128        }
129
130        if ( $this->updateRecentChanges ) {
131            $titleObj = SpecialPage::getTitleFor( 'Log', $this->type );
132
133            $recentChange = $recentChangeStore->createLogRecentChange(
134                $now, $titleObj, $this->performer, $this->getRcComment(), '',
135                $this->type, $this->action, $this->target, $this->comment,
136                $this->params, $newId, $this->getRcCommentIRC()
137            );
138            $recentChangeStore->insertRecentChange( $recentChange );
139        } elseif ( $this->sendToUDP ) {
140            // Notify external application via UDP.
141            // We send this to IRC but do not want to add it the RC table.
142            $titleObj = SpecialPage::getTitleFor( 'Log', $this->type );
143            $recentChange = $recentChangeStore->createLogRecentChange(
144                $now, $titleObj, $this->performer, $this->getRcComment(), '',
145                $this->type, $this->action, $this->target, $this->comment,
146                $this->params, $newId, $this->getRcCommentIRC()
147            );
148            $recentChangeRCFeedNotifier->notifyRCFeeds( $recentChange );
149        }
150
151        return $newId;
152    }
153
154    /**
155     * Get the RC comment from the last addEntry() call
156     *
157     * @return string
158     */
159    public function getRcComment() {
160        $rcComment = $this->actionText;
161
162        if ( $this->comment != '' ) {
163            if ( $rcComment == '' ) {
164                $rcComment = $this->comment;
165            } else {
166                $rcComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() .
167                    $this->comment;
168            }
169        }
170
171        return $rcComment;
172    }
173
174    /**
175     * Get the RC comment from the last addEntry() call for IRC
176     *
177     * @return string
178     */
179    public function getRcCommentIRC() {
180        $rcComment = $this->ircActionText;
181
182        if ( $this->comment != '' ) {
183            if ( $rcComment == '' ) {
184                $rcComment = $this->comment;
185            } else {
186                $rcComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() .
187                    $this->comment;
188            }
189        }
190
191        return $rcComment;
192    }
193
194    /**
195     * Get the comment from the last addEntry() call
196     * @return string
197     */
198    public function getComment() {
199        return $this->comment;
200    }
201
202    /**
203     * Get the list of valid log types
204     *
205     * @return string[]
206     */
207    public static function validTypes() {
208        $logTypes = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::LogTypes );
209
210        return $logTypes;
211    }
212
213    /**
214     * Is $type a valid log type
215     *
216     * @param string $type Log type to check
217     * @return bool
218     */
219    public static function isLogType( $type ) {
220        return in_array( $type, self::validTypes() );
221    }
222
223    /**
224     * Generate text for a log entry.
225     * Only LogFormatter should call this function.
226     *
227     * @param string $type Log type
228     * @param string $action Log action
229     * @param Title|null $title
230     * @param Skin|null $skin Skin object or null. If null, we want to use the wiki
231     *   content language, since that will go to the IRC feed.
232     * @param array $params
233     * @param bool $filterWikilinks Whether to filter wiki links
234     * @return string HTML
235     */
236    public static function actionText( $type, $action, $title = null, $skin = null,
237        $params = [], $filterWikilinks = false
238    ) {
239        $config = MediaWikiServices::getInstance()->getMainConfig();
240        $key = "$type/$action";
241
242        $logActions = $config->get( MainConfigNames::LogActions );
243
244        if ( isset( $logActions[$key] ) ) {
245            $message = $logActions[$key];
246        } else {
247            wfDebug( "LogPage::actionText - unknown action $key" );
248            $message = "log-unknown-action";
249            $params = [ $key ];
250        }
251
252        if ( $skin === null ) {
253            $langObj = MediaWikiServices::getInstance()->getContentLanguage();
254            $langObjOrNull = null;
255        } else {
256            $langObj = $langObjOrNull = $skin->getLanguage();
257        }
258        if ( $title === null ) {
259            $rv = wfMessage( $message )->inLanguage( $langObj )->escaped();
260        } else {
261            $titleLink = self::getTitleLink( $title, $langObjOrNull );
262
263            if ( count( $params ) == 0 ) {
264                $rv = wfMessage( $message )->rawParams( $titleLink )
265                    ->inLanguage( $langObj )->escaped();
266            } else {
267                array_unshift( $params, $titleLink );
268
269                $rv = wfMessage( $message )->rawParams( $params )
270                        ->inLanguage( $langObj )->escaped();
271            }
272        }
273
274        // For the perplexed, this feature was added in r7855 by Erik.
275        // The feature was added because we liked adding [[$1]] in our log entries
276        // but the log entries are parsed as Wikitext on RecentChanges but as HTML
277        // on Special:Log. The hack is essentially that [[$1]] represented a link
278        // to the title in question. The first parameter to the HTML version (Special:Log)
279        // is that link in HTML form, and so this just gets rid of the ugly [[]].
280        // However, this is a horrible hack and it doesn't work like you expect if, say,
281        // you want to link to something OTHER than the title of the log entry.
282        // The real problem, which Erik was trying to fix (and it sort-of works now) is
283        // that the same messages are being treated as both wikitext *and* HTML.
284        if ( $filterWikilinks ) {
285            $rv = str_replace( '[[', '', $rv );
286            $rv = str_replace( ']]', '', $rv );
287        }
288
289        return $rv;
290    }
291
292    /**
293     * @param Title $title
294     * @param ?Language $lang
295     * @return string HTML
296     */
297    private static function getTitleLink( Title $title, ?Language $lang ): string {
298        if ( !$lang ) {
299            return $title->getPrefixedText();
300        }
301
302        $services = MediaWikiServices::getInstance();
303        $linkRenderer = $services->getLinkRenderer();
304
305        if ( $title->isSpecialPage() ) {
306            [ $name, $par ] = $services->getSpecialPageFactory()->resolveAlias( $title->getDBkey() );
307
308            if ( $name === 'Log' ) {
309                $logPage = new LogPage( $par ?? '' );
310                return wfMessage( 'parentheses' )
311                    ->rawParams( $linkRenderer->makeLink( $title, $logPage->getName()->text() ) )
312                    ->inLanguage( $lang )
313                    ->escaped();
314            }
315        }
316
317        return $linkRenderer->makeLink( $title );
318    }
319
320    /**
321     * Add a log entry
322     *
323     * @param string $action One of '', 'block', 'protect', 'rights', 'delete',
324     *   'upload', 'move', 'move_redir'
325     * @param Title $target
326     * @param string|null $comment Description associated
327     * @param array $params Parameters passed later to wfMessage function
328     * @param int|UserIdentity $performer The user doing the action, or their user id.
329     *   Calling with user ID is deprecated since 1.36.
330     *
331     * @return int The log_id of the inserted log entry
332     */
333    public function addEntry( $action, $target, $comment, $params, $performer ) {
334        // FIXME $params is only documented to accept an array
335        if ( !is_array( $params ) ) {
336            $params = [ $params ];
337        }
338
339        # Trim spaces on user supplied text
340        $comment = trim( $comment ?? '' );
341
342        $this->action = $action;
343        $this->target = $target;
344        $this->comment = $comment;
345        $this->params = self::makeParamBlob( $params );
346
347        if ( !is_object( $performer ) ) {
348            $performer = User::newFromId( $performer );
349        }
350
351        $this->performer = $performer;
352
353        $logEntry = new ManualLogEntry( $this->type, $action );
354        $logEntry->setTarget( $target );
355        $logEntry->setPerformer( $performer );
356        $logEntry->setParameters( $params );
357        // All log entries using the LogPage to insert into the logging table
358        // are using the old logging system and therefore the legacy flag is
359        // needed to say the LogFormatter the parameters have numeric keys
360        $logEntry->setLegacy( true );
361
362        $formatter = MediaWikiServices::getInstance()->getLogFormatterFactory()->newFromEntry( $logEntry );
363        $context = RequestContext::newExtraneousContext( $target );
364        $formatter->setContext( $context );
365
366        $this->actionText = $formatter->getPlainActionText();
367        $this->ircActionText = $formatter->getIRCActionText();
368
369        return $this->saveContent();
370    }
371
372    /**
373     * Create a blob from a parameter array
374     *
375     * @param array $params
376     * @return string
377     */
378    public static function makeParamBlob( $params ) {
379        $params = array_values( $params );
380        $params['_legacy_'] = true;
381        return LogEntryBase::makeParamBlob( $params );
382    }
383
384    /**
385     * Extract a parameter array from a blob
386     *
387     * @param string $blob
388     * @return array
389     */
390    public static function extractParams( $blob ) {
391        if ( $blob === '' ) {
392            return [];
393        } else {
394            return explode( "\n", $blob );
395        }
396    }
397
398    /**
399     * Name of the log.
400     * @return Message
401     * @since 1.19
402     */
403    public function getName() {
404        $logNames = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::LogNames );
405
406        // BC
407        $key = $logNames[$this->type] ?? 'log-name-' . $this->type;
408
409        return wfMessage( $key );
410    }
411
412    /**
413     * Description of this log type.
414     * @return Message
415     * @since 1.19
416     */
417    public function getDescription() {
418        $logHeaders = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::LogHeaders );
419        // BC
420        $key = $logHeaders[$this->type] ?? 'log-description-' . $this->type;
421
422        return wfMessage( $key );
423    }
424
425    /**
426     * Returns the right needed to read this log type.
427     * @return string
428     * @since 1.19
429     */
430    public function getRestriction() {
431        $logRestrictions = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::LogRestrictions );
432        // The empty string fallback will
433        // always return true in permission check
434        return $logRestrictions[$this->type] ?? '';
435    }
436
437    /**
438     * Tells if this log is not viewable by all.
439     * @return bool
440     * @since 1.19
441     */
442    public function isRestricted() {
443        $restriction = $this->getRestriction();
444
445        return $restriction !== '' && $restriction !== '*';
446    }
447}
448
449/** @deprecated class alias since 1.44 */
450class_alias( LogPage::class, 'LogPage' );