Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
74.66% covered (warning)
74.66%
109 / 146
65.52% covered (warning)
65.52%
19 / 29
CRAP
0.00% covered (danger)
0.00%
0 / 1
ManualLogEntry
75.17% covered (warning)
75.17%
109 / 145
65.52% covered (warning)
65.52%
19 / 29
95.99
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setParameters
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addParameter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setRelations
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setPerformer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setTarget
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
4.94
 setTimestamp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setComment
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setAssociatedRevId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addTags
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 setIsPatrollable
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setLegacy
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setDeleted
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setForceBotFlag
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 insert
98.00% covered (success)
98.00%
49 / 50
0.00% covered (danger)
0.00%
0 / 1
9
 getRecentChange
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
1
 publish
45.95% covered (danger)
45.95%
17 / 37
0.00% covered (danger)
0.00%
0 / 1
39.69
 getType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSubtype
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getParameters
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPerformerIdentity
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTarget
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTimestamp
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getComment
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAssociatedRevId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTags
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIsPatrollable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isLegacy
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDeleted
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Contains a class for dealing with manual log entries
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @author Niklas Laxström
22 * @license GPL-2.0-or-later
23 * @since 1.19
24 */
25
26namespace MediaWiki\Logging;
27
28use InvalidArgumentException;
29use MediaWiki\ChangeTags\Taggable;
30use MediaWiki\Context\RequestContext;
31use MediaWiki\Deferred\DeferredUpdates;
32use MediaWiki\HookContainer\HookRunner;
33use MediaWiki\Linker\LinkTarget;
34use MediaWiki\MediaWikiServices;
35use MediaWiki\Page\PageReference;
36use MediaWiki\RecentChanges\RecentChange;
37use MediaWiki\SpecialPage\SpecialPage;
38use MediaWiki\Title\Title;
39use MediaWiki\User\UserIdentity;
40use RuntimeException;
41use UnexpectedValueException;
42use Wikimedia\Assert\Assert;
43use Wikimedia\Rdbms\IDatabase;
44
45/**
46 * Class for creating new log entries and inserting them into the database.
47 *
48 * @newable
49 * @note marked as newable in 1.35 for lack of a better alternative,
50 *       but should be changed to use the builder pattern or the
51 *       command pattern.
52 * @since 1.19
53 * @see https://www.mediawiki.org/wiki/Manual:Logging_to_Special:Log
54 */
55class ManualLogEntry extends LogEntryBase implements Taggable {
56    /** @var string Type of log entry */
57    protected $type;
58
59    /** @var string Sub type of log entry */
60    protected $subtype;
61
62    /** @var array Parameters for log entry */
63    protected $parameters = [];
64
65    /** @var array */
66    protected $relations = [];
67
68    /** @var UserIdentity Performer of the action for the log entry */
69    protected $performer;
70
71    /** @var Title Target title for the log entry */
72    protected $target;
73
74    /** @var string Timestamp of creation of the log entry */
75    protected $timestamp;
76
77    /** @var string Comment for the log entry */
78    protected $comment = '';
79
80    /** @var int A rev id associated to the log entry */
81    protected $revId = 0;
82
83    /** @var string[] Change tags add to the log entry */
84    protected $tags = [];
85
86    /** @var int|null Deletion state of the log entry */
87    protected $deleted;
88
89    /** @var int ID of the log entry */
90    protected $id;
91
92    /** @var bool Can this log entry be patrolled? */
93    protected $isPatrollable = false;
94
95    /** @var bool Whether this is a legacy log entry */
96    protected $legacy = false;
97
98    /** @var bool|null The bot flag in the recent changes will be set to this value */
99    protected $forceBotFlag = null;
100
101    /**
102     * @stable to call
103     * @since 1.19
104     * @param string $type Log type. Should match $wgLogTypes.
105     * @param string $subtype Log subtype (action). Should match $wgLogActions or
106     *   (together with $type) $wgLogActionsHandlers.
107     * @note
108     */
109    public function __construct( $type, $subtype ) {
110        $this->type = $type;
111        $this->subtype = $subtype;
112    }
113
114    /**
115     * Set extra log parameters.
116     *
117     * Takes an array in a parameter name => parameter value format. The array
118     * will be converted to string via serialize() and stored in the log_params
119     * database field. (If you want to store parameters in such a way that they
120     * can be targeted by DB queries, use setRelations() instead.)
121     *
122     * You can pass these parameters to the log action message by prefixing the
123     * keys with a number and optional type, using colons to separate the fields.
124     * The numbering should start with number 4 (matching the $4 message
125     * parameter), as the first three parameters are hardcoded for every message
126     * ($1 is a link to the username and user talk page of the performing user,
127     * $2 is just the username (for determining gender), $3 is a link to the
128     * target page).
129     *
130     * If you want to store stuff that should not be available in messages, don't
131     * prefix the array key with a number and don't use the colons. (Note that
132     * such parameters will still be publicly viewable via the API.)
133     *
134     * Example:
135     *   $entry->setParameters( [
136     *     // store and use in messages as $4
137     *     '4::color' => 'blue',
138     *     // store as is, use in messages as $5 with Message::numParam()
139     *     '5:number:count' => 3000,
140     *     // store but do not use in messages
141     *     'animal' => 'dog'
142     *   ] );
143     *
144     * Typically, these parameters will be used in the logentry-<type>-<subtype>
145     * message, but custom formatters, declared via $wgLogActionsHandlers, can
146     * override that.
147     *
148     * @since 1.19
149     * @param array $parameters Associative array
150     * @see LogFormatter::formatParameterValue() for valid parameter types and
151     *   their meanings.
152     * @see self::setRelations() for storing parameters in a way that can be searched.
153     * @see LogFormatter::getMessageKey() for determining which message these
154     *   parameters will be used in.
155     */
156    public function setParameters( $parameters ) {
157        $this->parameters = $parameters;
158    }
159
160    /**
161     * Add a parameter to the list already set.
162     *
163     * @see setParameters
164     * @since 1.44
165     *
166     * @param string $name
167     * @param mixed $value
168     */
169    public function addParameter( $name, $value ) {
170        $this->parameters[$name] = $value;
171    }
172
173    /**
174     * Declare arbitrary tag/value relations to this log entry.
175     * These will be stored in the log_search table and can be used
176     * to filter log entries later on.
177     *
178     * @param array $relations Map of (tag => (list of values|value)); values must be string.
179     *   When an array of values is given, a separate DB row will be created for each value.
180     * @since 1.22
181     */
182    public function setRelations( array $relations ) {
183        $this->relations = $relations;
184    }
185
186    /**
187     * Set the user that performed the action being logged.
188     *
189     * @since 1.19
190     * @param UserIdentity $performer
191     */
192    public function setPerformer( UserIdentity $performer ) {
193        $this->performer = $performer;
194    }
195
196    /**
197     * Set the title of the object changed.
198     *
199     * @param LinkTarget|PageReference $target calling with LinkTarget
200     *   is deprecated since 1.37
201     * @since 1.19
202     */
203    public function setTarget( $target ) {
204        if ( $target instanceof PageReference ) {
205            $this->target = Title::newFromPageReference( $target );
206        } elseif ( $target instanceof LinkTarget ) {
207            $this->target = Title::newFromLinkTarget( $target );
208        } else {
209            throw new InvalidArgumentException( "Invalid target provided" );
210        }
211    }
212
213    /**
214     * Set the timestamp of when the logged action took place.
215     *
216     * @since 1.19
217     * @param string $timestamp Can be in any format accepted by ConvertibleTimestamp
218     */
219    public function setTimestamp( $timestamp ) {
220        $this->timestamp = $timestamp;
221    }
222
223    /**
224     * Set a comment associated with the action being logged.
225     *
226     * @since 1.19
227     * @param string $comment
228     */
229    public function setComment( string $comment ) {
230        $this->comment = $comment;
231    }
232
233    /**
234     * Set an associated revision id.
235     *
236     * For example, the ID of the revision that was inserted to mark a page move
237     * or protection, file upload, etc.
238     *
239     * @since 1.27
240     * @param int $revId
241     */
242    public function setAssociatedRevId( $revId ) {
243        $this->revId = $revId;
244    }
245
246    /**
247     * Add change tags for the log entry
248     *
249     * @since 1.33
250     * @param string|string[]|null $tags Tags to apply
251     */
252    public function addTags( $tags ) {
253        if ( $tags === null ) {
254            return;
255        }
256
257        if ( is_string( $tags ) ) {
258            $tags = [ $tags ];
259        }
260        Assert::parameterElementType( 'string', $tags, 'tags' );
261        $this->tags = array_unique( array_merge( $this->tags, $tags ) );
262    }
263
264    /**
265     * Set whether this log entry should be made patrollable
266     * This shouldn't depend on config, only on whether there is full support
267     * in the software for patrolling this log entry.
268     * False by default
269     *
270     * @since 1.27
271     * @param bool $patrollable
272     */
273    public function setIsPatrollable( $patrollable ) {
274        $this->isPatrollable = (bool)$patrollable;
275    }
276
277    /**
278     * Set the 'legacy' flag
279     *
280     * @since 1.25
281     * @param bool $legacy
282     */
283    public function setLegacy( $legacy ) {
284        $this->legacy = $legacy;
285    }
286
287    /**
288     * Set the 'deleted' flag.
289     *
290     * @since 1.19
291     * @param int $deleted One of LogPage::DELETED_* bitfield constants
292     */
293    public function setDeleted( $deleted ) {
294        $this->deleted = $deleted;
295    }
296
297    /**
298     * Set the bot flag in the recent changes to this value.
299     *
300     * @since 1.40
301     * @param bool $forceBotFlag
302     */
303    public function setForceBotFlag( bool $forceBotFlag ): void {
304        $this->forceBotFlag = $forceBotFlag;
305    }
306
307    /**
308     * Insert the entry into the `logging` table.
309     *
310     * @param IDatabase|null $dbw
311     * @return int ID of the log entry
312     */
313    public function insert( ?IDatabase $dbw = null ) {
314        $services = MediaWikiServices::getInstance();
315        $dbw = $dbw ?: $services->getConnectionProvider()->getPrimaryDatabase();
316
317        $this->timestamp ??= wfTimestampNow();
318        $actorId = $services->getActorStore()->acquireActorId( $this->getPerformerIdentity(), $dbw );
319
320        // Trim spaces on user supplied text
321        $comment = trim( $this->getComment() ?? '' );
322
323        $params = $this->getParameters();
324        $relations = $this->relations;
325
326        // Additional fields for which there's no space in the database table schema
327        $revId = $this->getAssociatedRevId();
328        if ( $revId ) {
329            $params['associated_rev_id'] = $revId;
330            $relations['associated_rev_id'] = $revId;
331        }
332
333        $row = [
334            'log_type' => $this->getType(),
335            'log_action' => $this->getSubtype(),
336            'log_timestamp' => $dbw->timestamp( $this->getTimestamp() ),
337            'log_actor' => $actorId,
338            'log_namespace' => $this->getTarget()->getNamespace(),
339            'log_title' => $this->getTarget()->getDBkey(),
340            'log_page' => $this->getTarget()->getArticleID(),
341            'log_params' => LogEntryBase::makeParamBlob( $params ),
342        ];
343        if ( $this->deleted !== null ) {
344            $row['log_deleted'] = $this->deleted;
345        }
346        $row += $services->getCommentStore()->insert( $dbw, 'log_comment', $comment );
347
348        $dbw->newInsertQueryBuilder()
349            ->insertInto( 'logging' )
350            ->row( $row )
351            ->caller( __METHOD__ )
352            ->execute();
353        $this->id = $dbw->insertId();
354
355        $rows = [];
356        foreach ( $relations as $tag => $values ) {
357            if ( $tag === '' ) {
358                throw new UnexpectedValueException( "Got empty log search tag." );
359            }
360
361            if ( !is_array( $values ) ) {
362                $values = [ $values ];
363            }
364
365            foreach ( $values as $value ) {
366                $rows[] = [
367                    'ls_field' => $tag,
368                    'ls_value' => $value,
369                    'ls_log_id' => $this->id
370                ];
371            }
372        }
373        if ( count( $rows ) ) {
374            $dbw->newInsertQueryBuilder()
375                ->insertInto( 'log_search' )
376                ->ignore()
377                ->rows( $rows )
378                ->caller( __METHOD__ )
379                ->execute();
380        }
381
382        return $this->id;
383    }
384
385    /**
386     * Get a RecentChanges object for the log entry
387     *
388     * @param int $newId
389     * @return RecentChange
390     * @since 1.23
391     */
392    public function getRecentChange( $newId = 0 ) {
393        $formatter = MediaWikiServices::getInstance()->getLogFormatterFactory()->newFromEntry( $this );
394        $context = RequestContext::newExtraneousContext( $this->getTarget() );
395        $formatter->setContext( $context );
396
397        $logpage = SpecialPage::getTitleFor( 'Log', $this->getType() );
398
399        return RecentChange::newLogEntry(
400            $this->getTimestamp(),
401            $logpage,
402            $this->getPerformerIdentity(),
403            $formatter->getPlainActionText(),
404            '',
405            $this->getType(),
406            $this->getSubtype(),
407            $this->getTarget(),
408            $this->getComment(),
409            LogEntryBase::makeParamBlob( $this->getParameters() ),
410            $newId,
411            $formatter->getIRCActionComment(), // Used for IRC feeds
412            $this->getAssociatedRevId(), // Used for e.g. moves and uploads
413            $this->getIsPatrollable(),
414            $this->forceBotFlag
415        );
416    }
417
418    /**
419     * Publish the log entry.
420     *
421     * @param int $newId Id of the log entry.
422     * @param string $to One of: rcandudp (default), rc, udp
423     */
424    public function publish( $newId, $to = 'rcandudp' ) {
425        $canAddTags = true;
426        // FIXME: this code should be removed once all callers properly call publish()
427        if ( $to === 'udp' && !$newId && !$this->getAssociatedRevId() ) {
428            \MediaWiki\Logger\LoggerFactory::getInstance( 'logging' )->warning(
429                'newId and/or revId must be set when calling ManualLogEntry::publish()',
430                [
431                    'newId' => $newId,
432                    'to' => $to,
433                    'revId' => $this->getAssociatedRevId(),
434                    // pass a new exception to register the stack trace
435                    'exception' => new RuntimeException()
436                ]
437            );
438            $canAddTags = false;
439        }
440
441        $log = new LogPage( $this->getType() );
442        if ( !$log->isRestricted() ) {
443            // We need to generate a RecentChanges object now so that we can have the rc_bot attribute set based
444            // on any temporary user rights assigned to the user as part of the creation of this log entry.
445            // We do not attempt to save it to the DB until POSTSEND to avoid writes blocking a response (T127852).
446            ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
447                ->onManualLogEntryBeforePublish( $this );
448            $rc = $this->getRecentChange( $newId );
449
450            DeferredUpdates::addCallableUpdate(
451                function () use ( $newId, $to, $canAddTags, $rc ) {
452                    if ( $to === 'rc' || $to === 'rcandudp' ) {
453                        // save RC, passing tags so they are applied there
454                        $rc->addTags( $this->getTags() );
455                        $rc->save( $rc::SEND_NONE );
456                    } else {
457                        $tags = $this->getTags();
458                        if ( $tags && $canAddTags ) {
459                            $revId = $this->getAssociatedRevId();
460                            MediaWikiServices::getInstance()->getChangeTagsStore()->addTags(
461                                $tags,
462                                null,
463                                $revId > 0 ? $revId : null,
464                                $newId > 0 ? $newId : null
465                            );
466                        }
467                    }
468
469                    if ( $to === 'udp' || $to === 'rcandudp' ) {
470                        $rc->notifyRCFeeds();
471                    }
472                },
473                DeferredUpdates::POSTSEND,
474                MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase()
475            );
476        }
477    }
478
479    /**
480     * @return string
481     */
482    public function getType() {
483        return $this->type;
484    }
485
486    /**
487     * @return string
488     */
489    public function getSubtype() {
490        return $this->subtype;
491    }
492
493    /**
494     * @return array
495     */
496    public function getParameters() {
497        return $this->parameters;
498    }
499
500    public function getPerformerIdentity(): UserIdentity {
501        return $this->performer;
502    }
503
504    /**
505     * @return Title
506     */
507    public function getTarget() {
508        return $this->target;
509    }
510
511    /**
512     * @return string|false TS_MW timestamp, a string with 14 digits
513     */
514    public function getTimestamp() {
515        $ts = $this->timestamp ?? wfTimestampNow();
516
517        return wfTimestamp( TS_MW, $ts );
518    }
519
520    /**
521     * @return string
522     */
523    public function getComment() {
524        return $this->comment;
525    }
526
527    /**
528     * @since 1.27
529     * @return int
530     */
531    public function getAssociatedRevId() {
532        return $this->revId;
533    }
534
535    /**
536     * @since 1.27
537     * @return string[]
538     */
539    public function getTags() {
540        return $this->tags;
541    }
542
543    /**
544     * Whether this log entry is patrollable
545     *
546     * @since 1.27
547     * @return bool
548     */
549    public function getIsPatrollable() {
550        return $this->isPatrollable;
551    }
552
553    /**
554     * @since 1.25
555     * @return bool
556     */
557    public function isLegacy() {
558        return $this->legacy;
559    }
560
561    /**
562     * @return int
563     */
564    public function getDeleted() {
565        return (int)$this->deleted;
566    }
567}
568
569/** @deprecated class alias since 1.44 */
570class_alias( ManualLogEntry::class, 'ManualLogEntry' );