Issues (1401)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

repo/includes/EditEntity/MediawikiEditEntity.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
namespace Wikibase\Repo\EditEntity;
4
5
use InvalidArgumentException;
6
use MediaWiki\MediaWikiServices;
7
use MWException;
8
use ReadOnlyError;
9
use RuntimeException;
10
use Status;
11
use Title;
12
use User;
13
use Wikibase\DataModel\Entity\EntityDocument;
14
use Wikibase\DataModel\Entity\EntityId;
15
use Wikibase\DataModel\Services\Diff\EntityDiffer;
16
use Wikibase\DataModel\Services\Diff\EntityPatcher;
17
use Wikibase\Lib\Store\EntityContentTooBigException;
18
use Wikibase\Lib\Store\EntityRevision;
19
use Wikibase\Lib\Store\EntityRevisionLookup;
20
use Wikibase\Lib\Store\EntityStore;
21
use Wikibase\Lib\Store\LookupConstants;
22
use Wikibase\Lib\Store\RevisionedUnresolvedRedirectException;
23
use Wikibase\Lib\Store\StorageException;
24
use Wikibase\Repo\Store\EntityPermissionChecker;
25
use Wikibase\Repo\Store\EntityTitleStoreLookup;
26
use Wikibase\Repo\WikibaseRepo;
27
28
/**
29
 * Handler for editing activity, providing a unified interface for saving modified entities while performing
30
 * permission checks and handling edit conflicts.
31
 *
32
 * @license GPL-2.0-or-later
33
 * @author John Erling Blad < [email protected] >
34
 * @author Daniel Kinzler
35
 * @author Thiemo Kreuz
36
 */
37
class MediawikiEditEntity implements EditEntity {
38
39
	/**
40
	 * @var EntityRevisionLookup
41
	 */
42
	private $entityRevisionLookup;
43
44
	/**
45
	 * @var EntityTitleStoreLookup
46
	 */
47
	private $titleLookup;
48
49
	/**
50
	 * @var EntityStore
51
	 */
52
	private $entityStore;
53
54
	/**
55
	 * @var EntityPermissionChecker
56
	 */
57
	private $permissionChecker;
58
59
	/**
60
	 * @var EntityDiffer
61
	 */
62
	private $entityDiffer;
63
64
	/**
65
	 * @var EntityPatcher
66
	 */
67
	private $entityPatcher;
68
69
	/**
70
	 * The ID of the entity to edit. May be null if a new entity is being created.
71
	 *
72
	 * @var EntityId|null
73
	 */
74
	private $entityId = null;
75
76
	/**
77
	 * @var EntityRevision|null
78
	 */
79
	private $baseRev = null;
80
81
	/**
82
	 * @var int|bool
83
	 */
84
	private $baseRevId;
85
86
	/**
87
	 * @var EntityRevision|null
88
	 */
89
	private $latestRev = null;
90
91
	/**
92
	 * @var int
93
	 */
94
	private $latestRevId = 0;
95
96
	/**
97
	 * @var Status|null
98
	 */
99
	private $status = null;
100
101
	/**
102
	 * @var User|null
103
	 */
104
	private $user = null;
105
106
	/**
107
	 * @var Title|null
108
	 */
109
	private $title = null;
110
111
	/**
112
	 * @var EditFilterHookRunner
113
	 */
114
	private $editFilterHookRunner;
115
116
	/**
117
	 * @var int Bit field for error types, using the EditEntity::XXX_ERROR constants.
118
	 */
119
	private $errorType = 0;
120
121
	/**
122
	 * @var int
123
	 */
124
	private $maxSerializedEntitySize;
125
126
	/**
127
	 * @var bool Can use a master connection or not
128
	 */
129
	private $allowMasterConnection;
130
131
	/**
132
	 * @param EntityTitleStoreLookup $titleLookup
133
	 * @param EntityRevisionLookup $entityLookup
134
	 * @param EntityStore $entityStore
135
	 * @param EntityPermissionChecker $permissionChecker
136
	 * @param EntityDiffer $entityDiffer
137
	 * @param EntityPatcher $entityPatcher
138
	 * @param EntityId|null $entityId the ID of the entity being edited.
139
	 *        May be null when creating a new entity.
140
	 * @param User $user the user performing the edit
141
	 * @param EditFilterHookRunner $editFilterHookRunner
142
	 * @param int $maxSerializedEntitySize the maximal allowed entity size in Kilobytes
143
	 * @param int $baseRevId the base revision ID for conflict checking.
144
	 *        Use 0 to indicate that the current revision should be used as the base revision,
145
	 *        effectively disabling conflict detections. true and false will be accepted for
146
	 *        backwards compatibility, but both will be treated like 0. Note that the behavior
147
	 *        of this class changed so that "late" conflicts that arise between edit conflict
148
	 *        detection and database update are always detected, and result in the update to fail.
149
	 * @param bool $allowMasterConnection
150
	 */
151
	public function __construct(
152
		EntityTitleStoreLookup $titleLookup,
153
		EntityRevisionLookup $entityLookup,
154
		EntityStore $entityStore,
155
		EntityPermissionChecker $permissionChecker,
156
		EntityDiffer $entityDiffer,
157
		EntityPatcher $entityPatcher,
158
		?EntityId $entityId,
159
		User $user,
160
		EditFilterHookRunner $editFilterHookRunner,
161
		$maxSerializedEntitySize,
162
		$baseRevId = 0,
163
		$allowMasterConnection = true
164
	) {
165
		$this->entityId = $entityId;
166
167
		if ( is_string( $baseRevId ) ) {
168
			$baseRevId = (int)$baseRevId;
169
		}
170
171
		if ( is_bool( $baseRevId ) ) {
172
			$baseRevId = 0;
173
		}
174
175
		$this->user = $user;
176
		$this->baseRevId = $baseRevId;
177
178
		$this->errorType = 0;
179
		$this->status = Status::newGood();
180
181
		$this->titleLookup = $titleLookup;
182
		$this->entityRevisionLookup = $entityLookup;
183
		$this->entityStore = $entityStore;
184
		$this->permissionChecker = $permissionChecker;
185
		$this->entityDiffer = $entityDiffer;
186
		$this->entityPatcher = $entityPatcher;
187
188
		$this->editFilterHookRunner = $editFilterHookRunner;
189
		$this->allowMasterConnection = $allowMasterConnection;
190
		$this->maxSerializedEntitySize = $maxSerializedEntitySize;
191
	}
192
193
	/**
194
	 * Returns the ID of the entity being edited.
195
	 * May be null if a new entity is to be created.
196
	 *
197
	 * @return null|EntityId
198
	 */
199
	public function getEntityId() {
200
		return $this->entityId;
201
	}
202
203
	/**
204
	 * Returns the Title of the page holding the entity that is being edited.
205
	 *
206
	 * @return Title|null
207
	 */
208
	private function getTitle() {
209
		if ( $this->title === null ) {
210
			$id = $this->getEntityId();
211
212
			if ( $id !== null ) {
213
				$this->title = $this->titleLookup->getTitleForId( $id );
214
			}
215
		}
216
217
		return $this->title;
218
	}
219
220
	/**
221
	 * Returns the latest revision of the entity.
222
	 *
223
	 * @return EntityRevision|null
224
	 */
225
	public function getLatestRevision() {
226
		if ( $this->latestRev === null ) {
227
			$id = $this->getEntityId();
228
229
			if ( $id !== null ) {
230
				// NOTE: It's important to remember this, if someone calls clear() on
231
				// $this->getPage(), this should NOT change!
232
				$this->latestRev = $this->entityRevisionLookup->getEntityRevision(
233
					$id,
234
					0,
235
					$this->getReplicaMode()
236
				);
237
			}
238
		}
239
240
		return $this->latestRev;
241
	}
242
243
	/**
244
	 * @return int 0 if the entity doesn't exist
245
	 */
246
	private function getLatestRevisionId() {
247
		// Don't do negative caching: We call this to see whether the entity yet exists
248
		// before creating.
249
		if ( $this->latestRevId === 0 ) {
250
			$id = $this->getEntityId();
251
252
			if ( $this->latestRev !== null ) {
253
				$this->latestRevId = $this->latestRev->getRevisionId();
254
			} elseif ( $id !== null ) {
255
				$result = $this->entityRevisionLookup->getLatestRevisionId(
256
					$id,
257
					$this->getReplicaMode()
258
				);
259
				$returnZero = function () {
260
					return 0;
261
				};
262
				$this->latestRevId = $result->onNonexistentEntity( $returnZero )
263
					->onRedirect( $returnZero )
264
					->onConcreteRevision( 'intval' )
265
					->map();
266
			}
267
		}
268
269
		return $this->latestRevId;
270
	}
271
272
	/**
273
	 * Is the entity new?
274
	 *
275
	 * @return bool
276
	 */
277
	private function isNew() {
278
		return $this->getEntityId() === null || $this->getLatestRevisionId() === 0;
279
	}
280
281
	/**
282
	 * Does this entity belong to a new page?
283
	 * (An entity may {@link isNew be new}, and yet not belong to a new page,
284
	 * e. g. if it is stored in a non-main slot.)
285
	 *
286
	 * @return bool
287
	 */
288
	private function isNewPage(): bool {
289
		$title = $this->getTitle();
290
		if ( $title !== null ) {
291
			return !$title->exists();
292
		}
293
		return true;
294
	}
295
296
	/**
297
	 * Return the ID of the base revision for the edit. If no base revision ID was supplied to
298
	 * the constructor, this returns the ID of the latest revision. If the entity does not exist
299
	 * yet, this returns 0.
300
	 *
301
	 * @return int
302
	 */
303
	private function getBaseRevisionId() {
304
		if ( $this->baseRevId === 0 ) {
305
			$this->baseRevId = $this->getLatestRevisionId();
306
		}
307
308
		return $this->baseRevId;
309
	}
310
311
	/**
312
	 * Return the base revision for the edit. If no base revision ID was supplied to
313
	 * the constructor, this returns the latest revision. If the entity does not exist
314
	 * yet, this returns null.
315
	 *
316
	 * @return EntityRevision|null
317
	 * @throws MWException
318
	 */
319
	public function getBaseRevision() {
320
		if ( $this->baseRev === null ) {
321
			$baseRevId = $this->getBaseRevisionId();
322
323
			if ( $baseRevId === $this->getLatestRevisionId() ) {
324
				$this->baseRev = $this->getLatestRevision();
325
			} else {
326
				$id = $this->getEntityId();
327
328
				$this->baseRev = $this->entityRevisionLookup->getEntityRevision(
329
					$id,
330
					$baseRevId,
331
					$this->getReplicaMode()
332
				);
333
334
				if ( $this->baseRev === null ) {
335
					throw new MWException( 'Base revision ID not found: rev ' . $baseRevId
336
						. ' of ' . $id->getSerialization() );
337
				}
338
			}
339
		}
340
341
		return $this->baseRev;
342
	}
343
344
	/**
345
	 * @return string
346
	 */
347
	private function getReplicaMode() {
348
		if ( $this->allowMasterConnection === true ) {
349
			return LookupConstants::LATEST_FROM_REPLICA_WITH_FALLBACK;
350
		} else {
351
			return LookupConstants::LATEST_FROM_REPLICA;
352
		}
353
	}
354
355
	/**
356
	 * Get the status object. Only defined after attemptSave() was called.
357
	 *
358
	 * After a successful save, the Status object's value field will contain an array,
359
	 * just like the status returned by WikiPage::doEditContent(). Well known fields
360
	 * in the status value are:
361
	 *
362
	 *  - new: bool whether the edit created a new page
363
	 *  - revision: Revision the new revision object
364
	 *  - errorFlags: bit field indicating errors, see the XXX_ERROR constants.
365
	 *
366
	 * @return Status
367
	 */
368
	public function getStatus() {
369
		if ( $this->status === null ) {
370
			throw new RuntimeException( 'The status is undefined until attemptSave() has been called' );
371
		}
372
373
		return $this->status;
374
	}
375
376
	/**
377
	 * Determines whether the last call to attemptSave was successful.
378
	 *
379
	 * @return bool false if attemptSave() failed, true otherwise
380
	 */
381
	public function isSuccess() {
382
		return $this->errorType === 0 && $this->status->isOK();
383
	}
384
385
	/**
386
	 * Checks whether this EditEntity encountered any of the given error types while executing attemptSave().
387
	 *
388
	 * @param int $errorType bit field using the EditEntity::XXX_ERROR constants.
389
	 *            Defaults to EditEntity::ANY_ERROR.
390
	 *
391
	 * @return bool true if this EditEntity encountered any of the error types in $errorType, false otherwise.
392
	 */
393
	public function hasError( $errorType = EditEntity::ANY_ERROR ) {
394
		return ( $this->errorType & $errorType ) !== 0;
395
	}
396
397
	/**
398
	 * Determines whether an edit conflict exists, that is, whether another user has edited the
399
	 * same item after the base revision was created. In other words, this method checks whether
400
	 * the base revision (as provided to the constructor) is still current. If no base revision
401
	 * was provided to the constructor, this will always return false.
402
	 *
403
	 * If the base revision is different from the current revision, this will return true even if
404
	 * the edit conflict is resolvable. Indeed, it is used to determine whether conflict resolution
405
	 * should be attempted.
406
	 *
407
	 * @return bool
408
	 */
409
	public function hasEditConflict() {
410
		return !$this->isNew()
411
			&& $this->getBaseRevisionId() !== $this->getLatestRevisionId();
412
	}
413
414
	/**
415
	 * Attempts to fix an edit conflict by patching the intended change into the latest revision after
416
	 * checking for conflicts.
417
	 *
418
	 * @param EntityDocument $newEntity
419
	 *
420
	 * @throws MWException
421
	 * @return null|EntityDocument The patched Entity, or null if patching failed.
422
	 */
423
	private function fixEditConflict( EntityDocument $newEntity ) {
424
		$baseRev = $this->getBaseRevision();
425
		$latestRev = $this->getLatestRevision();
426
427
		if ( !$latestRev ) {
428
			wfLogWarning( 'Failed to load latest revision of entity ' . $newEntity->getId() . '!' );
429
			return null;
430
		}
431
432
		// calculate patch against base revision
433
		// NOTE: will fail if $baseRev or $base are null, which they may be if
434
		// this gets called at an inappropriate time. The data flow in this class
435
		// should be improved.
436
		$patch = $this->entityDiffer->diffEntities( $baseRev->getEntity(), $newEntity );
437
438
		if ( $patch->isEmpty() ) {
439
			// we didn't technically fix anything, but if there is nothing to change,
440
			// so just keep the current content as it is.
441
			return $latestRev->getEntity()->copy();
442
		}
443
444
		// apply the patch( base -> new ) to the latest revision.
445
		$patchedLatest = $latestRev->getEntity()->copy();
446
		$this->entityPatcher->patchEntity( $patchedLatest, $patch );
447
448
		// detect conflicts against latest revision
449
		$cleanPatch = $this->entityDiffer->diffEntities( $latestRev->getEntity(), $patchedLatest );
450
451
		$conflicts = $patch->count() - $cleanPatch->count();
452
453
		if ( $conflicts !== 0 ) {
454
			// patch doesn't apply cleanly
455
			if ( $this->userWasLastToEdit( $this->user, $newEntity->getId(), $this->getBaseRevisionId() ) ) {
456
				// it's a self-conflict
457
				if ( $cleanPatch->count() === 0 ) {
458
					// patch collapsed, possibly because of diff operation change from base to latest
459
					return null;
460
				} else {
461
					// we still have a working patch, try to apply
462
					$this->status->warning( 'wikibase-self-conflict-patched' );
463
				}
464
			} else {
465
				// there are unresolvable conflicts.
466
				return null;
467
			}
468
		} else {
469
			// can apply cleanly
470
			$this->status->warning( 'wikibase-conflict-patched' );
471
		}
472
473
		// return the patched entity
474
		return $patchedLatest;
475
	}
476
477
	/**
478
	 * Check if no edits were made by other users since the given revision.
479
	 * This makes the assumption that revision ids are monotonically increasing.
480
	 *
481
	 * @param User|null $user
482
	 * @param EntityId|null $entityId
483
	 * @param int|bool $lastRevId
484
	 *
485
	 * @return bool
486
	 */
487
	private function userWasLastToEdit( User $user = null, EntityId $entityId = null, $lastRevId = false ) {
488
		if ( $user === null || $entityId === null || $lastRevId === false ) {
489
			return false;
490
		}
491
492
		return $this->entityStore->userWasLastToEdit( $user, $entityId, $lastRevId );
493
	}
494
495
	/**
496
	 * Checks the necessary permissions to perform this edit.
497
	 * Per default, the 'edit' permission is checked.
498
	 * Use addRequiredPermission() to check more permissions.
499
	 *
500
	 * @param EntityDocument $newEntity
501
	 */
502
	private function checkEditPermissions( EntityDocument $newEntity ) {
503
		$permissionStatus = $this->permissionChecker->getPermissionStatusForEntity(
504
			$this->user,
505
			EntityPermissionChecker::ACTION_EDIT,
506
			$newEntity
507
		);
508
509
		$this->status->merge( $permissionStatus );
510
511
		if ( !$this->status->isOK() ) {
512
			$this->errorType |= EditEntity::PERMISSION_ERROR;
513
			$this->status->fatal( 'permissionserrors' );
514
		}
515
	}
516
517
	/**
518
	 * Checks if rate limits have been exceeded.
519
	 */
520
	private function checkRateLimits() {
521
		if ( $this->user->pingLimiter( 'edit' )
522
			|| ( $this->isNew() && $this->user->pingLimiter( 'create' ) )
523
		) {
524
			$this->errorType |= EditEntity::RATE_LIMIT;
525
			$this->status->fatal( 'actionthrottledtext' );
526
		}
527
	}
528
529
	/**
530
	 * Make sure the given WebRequest contains a valid edit token.
531
	 *
532
	 * @param string $token The token to check.
533
	 *
534
	 * @return bool true if the token is valid
535
	 */
536
	public function isTokenOK( $token ) {
537
		$tokenOk = $this->user->matchEditToken( $token );
538
		$tokenOkExceptSuffix = $this->user->matchEditTokenNoSuffix( $token );
539
540
		if ( !$tokenOk ) {
541
			if ( $tokenOkExceptSuffix ) {
542
				$this->status->fatal( 'token_suffix_mismatch' );
543
			} else {
544
				$this->status->fatal( 'session_fail_preview' );
545
			}
546
547
			$this->errorType |= EditEntity::TOKEN_ERROR;
548
			return false;
549
		}
550
551
		return true;
552
	}
553
554
	/**
555
	 * Resolve user specific default default for watch state, if $watch is null.
556
	 *
557
	 * @param boolean|null $watch
558
	 *
559
	 * @return bool
560
	 */
561
	private function getDesiredWatchState( $watch ) {
562
		if ( $watch === null ) {
563
			$watch = $this->getWatchDefault();
564
		}
565
566
		return $watch;
567
	}
568
569
	/**
570
	 * @param EntityId|null $id
571
	 *
572
	 * @throws InvalidArgumentException
573
	 */
574
	private function checkEntityId( EntityId $id = null ) {
575
		if ( $this->entityId ) {
576
			if ( !$this->entityId->equals( $id ) ) {
577
				throw new InvalidArgumentException(
578
					'Expected the EntityDocument to have ID ' . $this->entityId->getSerialization()
579
					. ', found ' . ( $id ? $id->getSerialization() : 'null' )
580
				);
581
			}
582
		}
583
	}
584
585
	/**
586
	 * @param EntityDocument $entity
587
	 *
588
	 * @throws ReadOnlyError
589
	 */
590
	private function checkReadOnly( EntityDocument $entity ) {
591
		if ( wfReadOnly() ) {
592
			throw new ReadOnlyError();
593
		}
594
		if ( $this->entityTypeIsReadOnly( $entity ) ) {
595
			MediaWikiServices::getInstance()->getConfiguredReadOnlyMode()->setReason(
596
				'Editing of entity type: ' . $entity->getType() . ' is currently disabled. It will be enabled soon.'
597
			);
598
			throw new ReadOnlyError();
599
		}
600
	}
601
602
	/**
603
	 * @param EntityDocument $entity
604
	 * @return bool
605
	 */
606
	private function entityTypeIsReadOnly( EntityDocument $entity ) {
607
		$readOnlyTypes = WikibaseRepo::getDefaultInstance()->getSettings()->getSetting( 'readOnlyEntityTypes' );
608
609
		return in_array( $entity->getType(), $readOnlyTypes );
610
	}
611
612
	/**
613
	 * Attempts to save the given Entity object.
614
	 *
615
	 * This method performs entity level permission checks, checks the edit toke, enforces rate
616
	 * limits, resolves edit conflicts, and updates user watchlists if appropriate.
617
	 *
618
	 * Success or failure are reported via the Status object returned by this method.
619
	 *
620
	 * @param EntityDocument $newEntity
621
	 * @param string $summary The edit summary.
622
	 * @param int $flags The EDIT_XXX flags as used by WikiPage::doEditContent().
623
	 *        Additionally, the EntityContent::EDIT_XXX constants can be used.
624
	 * @param string|bool $token Edit token to check, or false to disable the token check.
625
	 *                                Null will fail the token text, as will the empty string.
626
	 * @param bool|null $watch Whether the user wants to watch the entity.
627
	 *                                Set to null to apply default according to getWatchDefault().
628
	 * @param string[] $tags Change tags to add to the edit.
629
	 * Callers are responsible for checking that the user is permitted to add these tags
630
	 * (typically using {@link ChangeTags::canAddTagsAccompanyingChange}).
631
	 *
632
	 * @return Status
633
	 *
634
	 * @throws MWException
635
	 * @throws ReadOnlyError
636
	 *
637
	 * @see    WikiPage::doEditContent
638
	 * @see    EntityStore::saveEntity
639
	 */
640
	public function attemptSave( EntityDocument $newEntity, $summary, $flags, $token, $watch = null, array $tags = [] ) {
641
		$this->checkReadOnly( $newEntity );
642
		$this->checkEntityId( $newEntity->getId() );
643
644
		$watch = $this->getDesiredWatchState( $watch );
645
646
		$this->status = Status::newGood();
647
		$this->errorType = 0;
648
649
		if ( $token !== false && !$this->isTokenOK( $token ) ) {
650
			//@todo: This is redundant to the error code set in isTokenOK().
651
			//       We should figure out which error codes the callers expect,
652
			//       and only set the correct error code, in one place, probably here.
653
			$this->errorType |= EditEntity::TOKEN_ERROR;
654
			$this->status->fatal( 'sessionfailure' );
655
			$this->status->setResult( false, [ 'errorFlags' => $this->errorType ] );
656
			return $this->status;
657
		}
658
659
		$this->checkEditPermissions( $newEntity );
660
661
		$this->checkRateLimits(); // modifies $this->status
662
663
		if ( !$this->status->isOK() ) {
664
			$this->status->setResult( false, [ 'errorFlags' => $this->errorType ] );
665
			return $this->status;
666
		}
667
668
		// NOTE: Make sure the latest revision is loaded and cached.
669
		//      Would happen on demand anyway, but we want a well-defined point at which "latest" is
670
		//      frozen to a specific revision, just before the first check for edit conflicts.
671
		//      We can use the ID of the latest revision to protect against race conditions:
672
		//      if getLatestRevision() was called earlier by application logic, saving will fail
673
		//      if any new revisions were created between then and now.
674
		//      Note that this protection against "late" conflicts is unrelated to the detection
675
		//      of edit conflicts during user interaction, which use the base revision supplied
676
		//      to the constructor.
677
		try {
678
			$this->getLatestRevision();
679
		} catch ( RevisionedUnresolvedRedirectException $exception ) {
680
			$this->errorType |= EditEntity::PRECONDITION_FAILED;
681
			$this->status->fatal(
682
				'wikibase-save-unresolved-redirect',
683
				$exception->getEntityId()->getSerialization(),
684
				$exception->getRedirectTargetId()->getSerialization()
685
			);
686
			$this->status->setResult( false, [ 'errorFlags' => $this->errorType ] );
687
			return $this->status;
688
		}
689
690
		$raceProtectionRevId = $this->getLatestRevisionId();
691
692
		if ( $raceProtectionRevId === 0 ) {
693
			$raceProtectionRevId = false;
694
		}
695
696
		if ( $this->hasEditConflict() ) {
697
			$newEntity = $this->fixEditConflict( $newEntity );
698
699
			if ( !$newEntity ) {
700
				$this->errorType |= EditEntity::EDIT_CONFLICT_ERROR;
701
				$this->status->setResult( false, [ 'errorFlags' => $this->errorType ] );
702
				$this->status->error( 'edit-conflict' );
703
704
				return $this->status;
705
			}
706
		}
707
708
		if ( !$this->status->isOK() ) {
709
			$this->errorType |= EditEntity::PRECONDITION_FAILED;
710
		}
711
712
		if ( !$this->status->isOK() ) {
713
			$this->status->setResult( false, [ 'errorFlags' => $this->errorType ] );
714
			return $this->status;
715
		}
716
717
		try {
718
			$hookStatus = $this->editFilterHookRunner->run( $newEntity, $this->user, $summary );
719
		} catch ( EntityContentTooBigException $ex ) {
720
			$this->status->setResult( false, [ 'errorFlags' => $this->errorType ] );
721
			$this->status->error( wfMessage( 'wikibase-error-entity-too-big' )->sizeParams( $this->maxSerializedEntitySize * 1024 ) );
722
			return $this->status;
723
		}
724
		if ( !$hookStatus->isOK() ) {
725
			$this->errorType |= EditEntity::FILTERED;
726
		}
727
		$this->status->merge( $hookStatus );
728
729
		if ( !$this->status->isOK() ) {
730
			$this->status->setResult( false, [ 'errorFlags' => $this->errorType ] );
731
			return $this->status;
732
		}
733
734
		try {
735
			$entityRevision = $this->entityStore->saveEntity(
736
				$newEntity,
737
				$summary,
738
				$this->user,
739
				$flags | EDIT_AUTOSUMMARY,
740
				$raceProtectionRevId,
741
				$tags
742
			);
743
744
			$this->entityId = $newEntity->getId();
745
			$editStatus = Status::newGood( [ 'revision' => $entityRevision ] );
746
		} catch ( StorageException $ex ) {
747
			$editStatus = $ex->getStatus();
748
749
			if ( $editStatus === null ) {
750
				// XXX: perhaps internalerror_info isn't the best, but we need some generic error message.
751
				$editStatus = Status::newFatal( 'internalerror_info', $ex->getMessage() );
752
			}
753
754
			$this->errorType |= EditEntity::SAVE_ERROR;
755
		} catch ( EntityContentTooBigException $ex ) {
756
			$this->status->setResult( false, [ 'errorFlags' => $this->errorType ] );
757
			$this->status->error( wfMessage( 'wikibase-error-entity-too-big' )->sizeParams( $this->maxSerializedEntitySize * 1024 ) );
758
			return $this->status;
759
		}
760
761
		$this->status->setResult( $editStatus->isOK(), $editStatus->getValue() );
762
		$this->status->merge( $editStatus );
763
764
		if ( $this->status->isOK() ) {
765
			$this->updateWatchlist( $watch );
766
		} else {
767
			$value = $this->status->getValue();
768
			$value['errorFlags'] = $this->errorType;
769
			$this->status->setResult( false, $value );
770
		}
771
772
		return $this->status;
773
	}
774
775
	/**
776
	 * Returns whether the present edit would, per default,
777
	 * lead to the user watching the page.
778
	 *
779
	 * This uses the user's watchdefault and watchcreations settings
780
	 * and considers whether the entity is already watched by the user.
781
	 *
782
	 * @note Keep in sync with logic in EditPage!
783
	 *
784
	 * @return bool
785
	 */
786
	private function getWatchDefault() {
787
		// User wants to watch all edits or all creations.
788
		if ( $this->user->getOption( 'watchdefault' )
789
			|| ( $this->user->getOption( 'watchcreations' ) && $this->isNewPage() )
790
		) {
791
			return true;
792
		}
793
794
		// keep current state
795
		return $this->getEntityId() !== null &&
796
			$this->entityStore->isWatching( $this->user, $this->getEntityId() );
0 ignored issues
show
It seems like $this->user can be null; however, isWatching() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
797
	}
798
799
	/**
800
	 * Watches or unwatches the entity.
801
	 *
802
	 * @note Keep in sync with logic in EditPage!
803
	 * @todo move to separate service
804
	 *
805
	 * @param bool $watch whether to watch or unwatch the page.
806
	 *
807
	 * @throws MWException
808
	 */
809
	private function updateWatchlist( $watch ) {
810
		if ( $this->getTitle() === null ) {
811
			throw new MWException( 'Title not yet known!' );
812
		}
813
814
		$this->entityStore->updateWatchlist( $this->user, $this->getEntityId(), $watch );
815
	}
816
817
}
818