Completed
Branch master (939199)
by
unknown
39:35
created

includes/logging/LogEntry.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Contain classes for dealing with individual log entries
4
 *
5
 * This is how I see the log system history:
6
 * - appending to plain wiki pages
7
 * - formatting log entries based on database fields
8
 * - user is now part of the action message
9
 *
10
 * This program is free software; you can redistribute it and/or modify
11
 * it under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation; either version 2 of the License, or
13
 * (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
 * GNU General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU General Public License along
21
 * with this program; if not, write to the Free Software Foundation, Inc.,
22
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
23
 * http://www.gnu.org/copyleft/gpl.html
24
 *
25
 * @file
26
 * @author Niklas Laxström
27
 * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
28
 * @since 1.19
29
 */
30
31
/**
32
 * Interface for log entries. Every log entry has these methods.
33
 *
34
 * @since 1.19
35
 */
36
interface LogEntry {
37
38
	/**
39
	 * The main log type.
40
	 *
41
	 * @return string
42
	 */
43
	public function getType();
44
45
	/**
46
	 * The log subtype.
47
	 *
48
	 * @return string
49
	 */
50
	public function getSubtype();
51
52
	/**
53
	 * The full logtype in format maintype/subtype.
54
	 *
55
	 * @return string
56
	 */
57
	public function getFullType();
58
59
	/**
60
	 * Get the extra parameters stored for this message.
61
	 *
62
	 * @return array
63
	 */
64
	public function getParameters();
65
66
	/**
67
	 * Get the user for performed this action.
68
	 *
69
	 * @return User
70
	 */
71
	public function getPerformer();
72
73
	/**
74
	 * Get the target page of this action.
75
	 *
76
	 * @return Title
77
	 */
78
	public function getTarget();
79
80
	/**
81
	 * Get the timestamp when the action was executed.
82
	 *
83
	 * @return string
84
	 */
85
	public function getTimestamp();
86
87
	/**
88
	 * Get the user provided comment.
89
	 *
90
	 * @return string
91
	 */
92
	public function getComment();
93
94
	/**
95
	 * Get the access restriction.
96
	 *
97
	 * @return string
98
	 */
99
	public function getDeleted();
100
101
	/**
102
	 * @param int $field One of LogPage::DELETED_* bitfield constants
103
	 * @return bool
104
	 */
105
	public function isDeleted( $field );
106
}
107
108
/**
109
 * Extends the LogEntryInterface with some basic functionality
110
 *
111
 * @since 1.19
112
 */
113
abstract class LogEntryBase implements LogEntry {
114
115
	public function getFullType() {
116
		return $this->getType() . '/' . $this->getSubtype();
117
	}
118
119
	public function isDeleted( $field ) {
120
		return ( $this->getDeleted() & $field ) === $field;
121
	}
122
123
	/**
124
	 * Whether the parameters for this log are stored in new or
125
	 * old format.
126
	 *
127
	 * @return bool
128
	 */
129
	public function isLegacy() {
130
		return false;
131
	}
132
133
	/**
134
	 * Create a blob from a parameter array
135
	 *
136
	 * @since 1.26
137
	 * @param array $params
138
	 * @return string
139
	 */
140
	public static function makeParamBlob( $params ) {
141
		return serialize( (array)$params );
142
	}
143
144
	/**
145
	 * Extract a parameter array from a blob
146
	 *
147
	 * @since 1.26
148
	 * @param string $blob
149
	 * @return array
150
	 */
151
	public static function extractParams( $blob ) {
152
		return unserialize( $blob );
153
	}
154
}
155
156
/**
157
 * This class wraps around database result row.
158
 *
159
 * @since 1.19
160
 */
161
class DatabaseLogEntry extends LogEntryBase {
162
163
	/**
164
	 * Returns array of information that is needed for querying
165
	 * log entries. Array contains the following keys:
166
	 * tables, fields, conds, options and join_conds
167
	 *
168
	 * @return array
169
	 */
170
	public static function getSelectQueryData() {
171
		$tables = [ 'logging', 'user' ];
172
		$fields = [
173
			'log_id', 'log_type', 'log_action', 'log_timestamp',
174
			'log_user', 'log_user_text',
175
			'log_namespace', 'log_title', // unused log_page
176
			'log_comment', 'log_params', 'log_deleted',
177
			'user_id', 'user_name', 'user_editcount',
178
		];
179
180
		$joins = [
181
			// IPs don't have an entry in user table
182
			'user' => [ 'LEFT JOIN', 'log_user=user_id' ],
183
		];
184
185
		return [
186
			'tables' => $tables,
187
			'fields' => $fields,
188
			'conds' => [],
189
			'options' => [],
190
			'join_conds' => $joins,
191
		];
192
	}
193
194
	/**
195
	 * Constructs new LogEntry from database result row.
196
	 * Supports rows from both logging and recentchanges table.
197
	 *
198
	 * @param stdClass|array $row
199
	 * @return DatabaseLogEntry
200
	 */
201
	public static function newFromRow( $row ) {
202
		$row = (object)$row;
203
		if ( isset( $row->rc_logid ) ) {
204
			return new RCDatabaseLogEntry( $row );
205
		} else {
206
			return new self( $row );
207
		}
208
	}
209
210
	/** @var stdClass Database result row. */
211
	protected $row;
212
213
	/** @var User */
214
	protected $performer;
215
216
	/** @var array Parameters for log entry */
217
	protected $params;
218
219
	/** @var int A rev id associated to the log entry */
220
	protected $revId = null;
221
222
	/** @var bool Whether the parameters for this log entry are stored in new or old format. */
223
	protected $legacy;
224
225
	protected function __construct( $row ) {
226
		$this->row = $row;
227
	}
228
229
	/**
230
	 * Returns the unique database id.
231
	 *
232
	 * @return int
233
	 */
234
	public function getId() {
235
		return (int)$this->row->log_id;
236
	}
237
238
	/**
239
	 * Returns whatever is stored in the database field.
240
	 *
241
	 * @return string
242
	 */
243
	protected function getRawParameters() {
244
		return $this->row->log_params;
245
	}
246
247
	public function isLegacy() {
248
		// This extracts the property
249
		$this->getParameters();
250
		return $this->legacy;
251
	}
252
253
	public function getType() {
254
		return $this->row->log_type;
255
	}
256
257
	public function getSubtype() {
258
		return $this->row->log_action;
259
	}
260
261
	public function getParameters() {
262
		if ( !isset( $this->params ) ) {
263
			$blob = $this->getRawParameters();
264
			MediaWiki\suppressWarnings();
265
			$params = LogEntryBase::extractParams( $blob );
266
			MediaWiki\restoreWarnings();
267
			if ( $params !== false ) {
268
				$this->params = $params;
269
				$this->legacy = false;
270
			} else {
271
				$this->params = LogPage::extractParams( $blob );
272
				$this->legacy = true;
273
			}
274
275
			if ( isset( $this->params['associated_rev_id'] ) ) {
276
				$this->revId = $this->params['associated_rev_id'];
277
				unset( $this->params['associated_rev_id'] );
278
			}
279
		}
280
281
		return $this->params;
282
	}
283
284
	public function getAssociatedRevId() {
285
		// This extracts the property
286
		$this->getParameters();
287
		return $this->revId;
288
	}
289
290
	public function getPerformer() {
291
		if ( !$this->performer ) {
292
			$userId = (int)$this->row->log_user;
293 View Code Duplication
			if ( $userId !== 0 ) {
294
				// logged-in users
295
				if ( isset( $this->row->user_name ) ) {
296
					$this->performer = User::newFromRow( $this->row );
297
				} else {
298
					$this->performer = User::newFromId( $userId );
299
				}
300
			} else {
301
				// IP users
302
				$userText = $this->row->log_user_text;
303
				$this->performer = User::newFromName( $userText, false );
304
			}
305
		}
306
307
		return $this->performer;
308
	}
309
310
	public function getTarget() {
311
		$namespace = $this->row->log_namespace;
312
		$page = $this->row->log_title;
313
		$title = Title::makeTitle( $namespace, $page );
314
315
		return $title;
316
	}
317
318
	public function getTimestamp() {
319
		return wfTimestamp( TS_MW, $this->row->log_timestamp );
320
	}
321
322
	public function getComment() {
323
		return $this->row->log_comment;
324
	}
325
326
	public function getDeleted() {
327
		return $this->row->log_deleted;
328
	}
329
}
330
331
class RCDatabaseLogEntry extends DatabaseLogEntry {
332
333
	public function getId() {
334
		return $this->row->rc_logid;
335
	}
336
337
	protected function getRawParameters() {
338
		return $this->row->rc_params;
339
	}
340
341
	public function getAssociatedRevId() {
342
		return $this->row->rc_this_oldid;
343
	}
344
345
	public function getType() {
346
		return $this->row->rc_log_type;
347
	}
348
349
	public function getSubtype() {
350
		return $this->row->rc_log_action;
351
	}
352
353
	public function getPerformer() {
354 View Code Duplication
		if ( !$this->performer ) {
355
			$userId = (int)$this->row->rc_user;
356
			if ( $userId !== 0 ) {
357
				$this->performer = User::newFromId( $userId );
358
			} else {
359
				$userText = $this->row->rc_user_text;
360
				// Might be an IP, don't validate the username
361
				$this->performer = User::newFromName( $userText, false );
362
			}
363
		}
364
365
		return $this->performer;
366
	}
367
368
	public function getTarget() {
369
		$namespace = $this->row->rc_namespace;
370
		$page = $this->row->rc_title;
371
		$title = Title::makeTitle( $namespace, $page );
372
373
		return $title;
374
	}
375
376
	public function getTimestamp() {
377
		return wfTimestamp( TS_MW, $this->row->rc_timestamp );
378
	}
379
380
	public function getComment() {
381
		return $this->row->rc_comment;
382
	}
383
384
	public function getDeleted() {
385
		return $this->row->rc_deleted;
386
	}
387
}
388
389
/**
390
 * Class for creating log entries manually, to inject them into the database.
391
 *
392
 * @since 1.19
393
 */
394
class ManualLogEntry extends LogEntryBase {
395
	/** @var string Type of log entry */
396
	protected $type;
397
398
	/** @var string Sub type of log entry */
399
	protected $subtype;
400
401
	/** @var array Parameters for log entry */
402
	protected $parameters = [];
403
404
	/** @var array */
405
	protected $relations = [];
406
407
	/** @var User Performer of the action for the log entry */
408
	protected $performer;
409
410
	/** @var Title Target title for the log entry */
411
	protected $target;
412
413
	/** @var string Timestamp of creation of the log entry */
414
	protected $timestamp;
415
416
	/** @var string Comment for the log entry */
417
	protected $comment = '';
418
419
	/** @var int A rev id associated to the log entry */
420
	protected $revId = 0;
421
422
	/** @var array Change tags add to the log entry */
423
	protected $tags = null;
424
425
	/** @var int Deletion state of the log entry */
426
	protected $deleted;
427
428
	/** @var int ID of the log entry */
429
	protected $id;
430
431
	/** @var Can this log entry be patrolled? */
432
	protected $isPatrollable = false;
433
434
	/** @var bool Whether this is a legacy log entry */
435
	protected $legacy = false;
436
437
	/**
438
	 * Constructor.
439
	 *
440
	 * @since 1.19
441
	 * @param string $type
442
	 * @param string $subtype
443
	 */
444
	public function __construct( $type, $subtype ) {
445
		$this->type = $type;
446
		$this->subtype = $subtype;
447
	}
448
449
	/**
450
	 * Set extra log parameters.
451
	 *
452
	 * You can pass params to the log action message by prefixing the keys with
453
	 * a number and optional type, using colons to separate the fields. The
454
	 * numbering should start with number 4, the first three parameters are
455
	 * hardcoded for every message.
456
	 *
457
	 * If you want to store stuff that should not be available in messages, don't
458
	 * prefix the array key with a number and don't use the colons.
459
	 *
460
	 * Example:
461
	 *   $entry->setParameters(
462
	 *     '4::color' => 'blue',
463
	 *     '5:number:count' => 3000,
464
	 *     'animal' => 'dog'
465
	 *   );
466
	 *
467
	 * @since 1.19
468
	 * @param array $parameters Associative array
469
	 */
470
	public function setParameters( $parameters ) {
471
		$this->parameters = $parameters;
472
	}
473
474
	/**
475
	 * Declare arbitrary tag/value relations to this log entry.
476
	 * These can be used to filter log entries later on.
477
	 *
478
	 * @param array $relations Map of (tag => (list of values|value))
479
	 * @since 1.22
480
	 */
481
	public function setRelations( array $relations ) {
482
		$this->relations = $relations;
483
	}
484
485
	/**
486
	 * Set the user that performed the action being logged.
487
	 *
488
	 * @since 1.19
489
	 * @param User $performer
490
	 */
491
	public function setPerformer( User $performer ) {
492
		$this->performer = $performer;
493
	}
494
495
	/**
496
	 * Set the title of the object changed.
497
	 *
498
	 * @since 1.19
499
	 * @param Title $target
500
	 */
501
	public function setTarget( Title $target ) {
502
		$this->target = $target;
503
	}
504
505
	/**
506
	 * Set the timestamp of when the logged action took place.
507
	 *
508
	 * @since 1.19
509
	 * @param string $timestamp
510
	 */
511
	public function setTimestamp( $timestamp ) {
512
		$this->timestamp = $timestamp;
513
	}
514
515
	/**
516
	 * Set a comment associated with the action being logged.
517
	 *
518
	 * @since 1.19
519
	 * @param string $comment
520
	 */
521
	public function setComment( $comment ) {
522
		$this->comment = $comment;
523
	}
524
525
	/**
526
	 * Set an associated revision id.
527
	 *
528
	 * For example, the ID of the revision that was inserted to mark a page move
529
	 * or protection, file upload, etc.
530
	 *
531
	 * @since 1.27
532
	 * @param int $revId
533
	 */
534
	public function setAssociatedRevId( $revId ) {
535
		$this->revId = $revId;
536
	}
537
538
	/**
539
	 * Set change tags for the log entry.
540
	 *
541
	 * @since 1.27
542
	 * @param string|string[] $tags
543
	 */
544
	public function setTags( $tags ) {
545
		if ( is_string( $tags ) ) {
546
			$tags = [ $tags ];
547
		}
548
		$this->tags = $tags;
549
	}
550
551
	/**
552
	 * Set whether this log entry should be made patrollable
553
	 * This shouldn't depend on config, only on whether there is full support
554
	 * in the software for patrolling this log entry.
555
	 * False by default
556
	 *
557
	 * @since 1.27
558
	 * @param bool $patrollable
559
	 */
560
	public function setIsPatrollable( $patrollable ) {
561
		$this->isPatrollable = (bool)$patrollable;
0 ignored issues
show
Documentation Bug introduced by
It seems like (bool) $patrollable of type boolean is incompatible with the declared type object<Can> of property $isPatrollable.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
562
	}
563
564
	/**
565
	 * Set the 'legacy' flag
566
	 *
567
	 * @since 1.25
568
	 * @param bool $legacy
569
	 */
570
	public function setLegacy( $legacy ) {
571
		$this->legacy = $legacy;
572
	}
573
574
	/**
575
	 * Set the 'deleted' flag.
576
	 *
577
	 * @since 1.19
578
	 * @param int $deleted One of LogPage::DELETED_* bitfield constants
579
	 */
580
	public function setDeleted( $deleted ) {
581
		$this->deleted = $deleted;
582
	}
583
584
	/**
585
	 * Insert the entry into the `logging` table.
586
	 *
587
	 * @param IDatabase $dbw
588
	 * @return int ID of the log entry
589
	 * @throws MWException
590
	 */
591
	public function insert( IDatabase $dbw = null ) {
592
		global $wgContLang;
593
594
		$dbw = $dbw ?: wfGetDB( DB_MASTER );
595
		$id = $dbw->nextSequenceValue( 'logging_log_id_seq' );
596
597
		if ( $this->timestamp === null ) {
598
			$this->timestamp = wfTimestampNow();
599
		}
600
601
		// Trim spaces on user supplied text
602
		$comment = trim( $this->getComment() );
603
604
		// Truncate for whole multibyte characters.
605
		$comment = $wgContLang->truncate( $comment, 255 );
606
607
		$params = $this->getParameters();
608
		$relations = $this->relations;
609
610
		// Additional fields for which there's no space in the database table schema
611
		$revId = $this->getAssociatedRevId();
612
		if ( $revId ) {
613
			$params['associated_rev_id'] = $revId;
614
			$relations['associated_rev_id'] = $revId;
615
		}
616
617
		$data = [
618
			'log_id' => $id,
619
			'log_type' => $this->getType(),
620
			'log_action' => $this->getSubtype(),
621
			'log_timestamp' => $dbw->timestamp( $this->getTimestamp() ),
622
			'log_user' => $this->getPerformer()->getId(),
623
			'log_user_text' => $this->getPerformer()->getName(),
624
			'log_namespace' => $this->getTarget()->getNamespace(),
625
			'log_title' => $this->getTarget()->getDBkey(),
626
			'log_page' => $this->getTarget()->getArticleID(),
627
			'log_comment' => $comment,
628
			'log_params' => LogEntryBase::makeParamBlob( $params ),
629
		];
630
		if ( isset( $this->deleted ) ) {
631
			$data['log_deleted'] = $this->deleted;
632
		}
633
634
		$dbw->insert( 'logging', $data, __METHOD__ );
635
		$this->id = !is_null( $id ) ? $id : $dbw->insertId();
636
637
		$rows = [];
638
		foreach ( $relations as $tag => $values ) {
639
			if ( !strlen( $tag ) ) {
640
				throw new MWException( "Got empty log search tag." );
641
			}
642
643
			if ( !is_array( $values ) ) {
644
				$values = [ $values ];
645
			}
646
647 View Code Duplication
			foreach ( $values as $value ) {
648
				$rows[] = [
649
					'ls_field' => $tag,
650
					'ls_value' => $value,
651
					'ls_log_id' => $this->id
652
				];
653
			}
654
		}
655
		if ( count( $rows ) ) {
656
			$dbw->insert( 'log_search', $rows, __METHOD__, 'IGNORE' );
657
		}
658
659
		return $this->id;
660
	}
661
662
	/**
663
	 * Get a RecentChanges object for the log entry
664
	 *
665
	 * @param int $newId
666
	 * @return RecentChange
667
	 * @since 1.23
668
	 */
669
	public function getRecentChange( $newId = 0 ) {
670
		$formatter = LogFormatter::newFromEntry( $this );
671
		$context = RequestContext::newExtraneousContext( $this->getTarget() );
672
		$formatter->setContext( $context );
673
674
		$logpage = SpecialPage::getTitleFor( 'Log', $this->getType() );
675
		$user = $this->getPerformer();
676
		$ip = "";
677
		if ( $user->isAnon() ) {
678
			// "MediaWiki default" and friends may have
679
			// no IP address in their name
680
			if ( IP::isIPAddress( $user->getName() ) ) {
681
				$ip = $user->getName();
682
			}
683
		}
684
685
		return RecentChange::newLogEntry(
686
			$this->getTimestamp(),
687
			$logpage,
688
			$user,
689
			$formatter->getPlainActionText(),
690
			$ip,
691
			$this->getType(),
692
			$this->getSubtype(),
693
			$this->getTarget(),
694
			$this->getComment(),
695
			LogEntryBase::makeParamBlob( $this->getParameters() ),
696
			$newId,
697
			$formatter->getIRCActionComment(), // Used for IRC feeds
698
			$this->getAssociatedRevId(), // Used for e.g. moves and uploads
699
			$this->getIsPatrollable()
700
		);
701
	}
702
703
	/**
704
	 * Publish the log entry.
705
	 *
706
	 * @param int $newId Id of the log entry.
707
	 * @param string $to One of: rcandudp (default), rc, udp
708
	 */
709
	public function publish( $newId, $to = 'rcandudp' ) {
710
		DeferredUpdates::addCallableUpdate(
711
			function () use ( $newId, $to ) {
712
				$log = new LogPage( $this->getType() );
713
				if ( !$log->isRestricted() ) {
714
					$rc = $this->getRecentChange( $newId );
715
716
					if ( $to === 'rc' || $to === 'rcandudp' ) {
717
						// save RC, passing tags so they are applied there
718
						$tags = $this->getTags();
719
						if ( is_null( $tags ) ) {
720
							$tags = [];
721
						}
722
						$rc->addTags( $tags );
723
						$rc->save( 'pleasedontudp' );
724
					}
725
726
					if ( $to === 'udp' || $to === 'rcandudp' ) {
727
						$rc->notifyRCFeeds();
728
					}
729
730
					// Log the autopatrol if the log entry is patrollable
731
					if ( $this->getIsPatrollable() &&
732
						$rc->getAttribute( 'rc_patrolled' ) === 1
733
					) {
734
						PatrolLog::record( $rc, true, $this->getPerformer() );
735
					}
736
				}
737
			},
738
			DeferredUpdates::POSTSEND,
739
			wfGetDB( DB_MASTER )
740
		);
741
	}
742
743
	public function getType() {
744
		return $this->type;
745
	}
746
747
	public function getSubtype() {
748
		return $this->subtype;
749
	}
750
751
	public function getParameters() {
752
		return $this->parameters;
753
	}
754
755
	/**
756
	 * @return User
757
	 */
758
	public function getPerformer() {
759
		return $this->performer;
760
	}
761
762
	/**
763
	 * @return Title
764
	 */
765
	public function getTarget() {
766
		return $this->target;
767
	}
768
769
	public function getTimestamp() {
770
		$ts = $this->timestamp !== null ? $this->timestamp : wfTimestampNow();
771
772
		return wfTimestamp( TS_MW, $ts );
773
	}
774
775
	public function getComment() {
776
		return $this->comment;
777
	}
778
779
	/**
780
	 * @since 1.27
781
	 * @return int
782
	 */
783
	public function getAssociatedRevId() {
784
		return $this->revId;
785
	}
786
787
	/**
788
	 * @since 1.27
789
	 * @return array
790
	 */
791
	public function getTags() {
792
		return $this->tags;
793
	}
794
795
	/**
796
	 * Whether this log entry is patrollable
797
	 *
798
	 * @since 1.27
799
	 * @return bool
800
	 */
801
	public function getIsPatrollable() {
802
		return $this->isPatrollable;
803
	}
804
805
	/**
806
	 * @since 1.25
807
	 * @return bool
808
	 */
809
	public function isLegacy() {
810
		return $this->legacy;
811
	}
812
813
	public function getDeleted() {
814
		return (int)$this->deleted;
815
	}
816
}
817