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