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