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