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