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