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