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/Api/ModifyEntity.php (6 issues)

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
declare( strict_types = 1 );
4
5
namespace Wikibase\Repo\Api;
6
7
use ApiBase;
8
use ApiMain;
9
use ApiUsageException;
10
use LogicException;
11
use MWContentSerializationException;
12
use Status;
13
use User;
14
use Wikibase\DataModel\Entity\EntityDocument;
15
use Wikibase\DataModel\Entity\EntityId;
16
use Wikibase\Lib\Store\EntityRevisionLookup;
17
use Wikibase\Lib\Store\LookupConstants;
18
use Wikibase\Lib\StringNormalizer;
19
use Wikibase\Lib\Summary;
20
use Wikibase\Repo\ChangeOp\ChangeOp;
21
use Wikibase\Repo\ChangeOp\ChangeOpException;
22
use Wikibase\Repo\ChangeOp\ChangeOpResult;
23
use Wikibase\Repo\ChangeOp\ChangeOpValidationException;
24
use Wikibase\Repo\SiteLinkTargetProvider;
25
use Wikibase\Repo\Store\EntityPermissionChecker;
26
use Wikibase\Repo\Store\EntityTitleStoreLookup;
27
use Wikibase\Repo\Store\Store;
28
use Wikibase\Repo\WikibaseRepo;
29
30
/**
31
 * Base class for API modules modifying a single entity identified based on id xor a combination of site and page title.
32
 *
33
 * @license GPL-2.0-or-later
34
 * @author John Erling Blad < [email protected] >
35
 * @author Daniel Kinzler
36
 * @author Michał Łazowik
37
 */
38
abstract class ModifyEntity extends ApiBase {
39
40
	use FederatedPropertyApiValidatorTrait;
41
42
	/**
43
	 * @var StringNormalizer
44
	 */
45
	protected $stringNormalizer;
46
47
	/**
48
	 * @var SiteLinkTargetProvider
49
	 */
50
	protected $siteLinkTargetProvider;
51
52
	/**
53
	 * @var EntityTitleStoreLookup
54
	 */
55
	private $titleLookup;
56
57
	/**
58
	 * @var string[]
59
	 */
60
	protected $siteLinkGroups;
61
62
	/**
63
	 * @var string[]
64
	 */
65
	protected $badgeItems;
66
67
	/**
68
	 * @var ApiErrorReporter
69
	 */
70
	protected $errorReporter;
71
72
	/**
73
	 * @var EntityPermissionChecker
74
	 */
75
	private $permissionChecker;
76
77
	/**
78
	 * @var EntityRevisionLookup
79
	 */
80
	private $revisionLookup;
81
82
	/**
83
	 * @var ResultBuilder
84
	 */
85
	private $resultBuilder;
86
87
	/**
88
	 * @var EntitySavingHelper
89
	 */
90
	private $entitySavingHelper;
91
92
	/**
93
	 * @var string[]
94
	 */
95
	protected $enabledEntityTypes;
96
97
	/**
98
	 * @param ApiMain $mainModule
99
	 * @param string $moduleName
100
	 * @param bool $federatedPropertiesEnabled
101
	 * @param string $modulePrefix
102
	 *
103
	 * @see ApiBase::__construct
104
	 */
105
	public function __construct( ApiMain $mainModule, string $moduleName, bool $federatedPropertiesEnabled, string $modulePrefix = '' ) {
106
		parent::__construct( $mainModule, $moduleName, $modulePrefix );
107
108
		$wikibaseRepo = WikibaseRepo::getDefaultInstance();
109
		$apiHelperFactory = $wikibaseRepo->getApiHelperFactory( $this->getContext() );
110
		$settings = $wikibaseRepo->getSettings();
111
112
		//TODO: provide a mechanism to override the services
113
		$this->errorReporter = $apiHelperFactory->getErrorReporter( $this );
114
		$this->resultBuilder = $apiHelperFactory->getResultBuilder( $this );
115
		$this->entitySavingHelper = $apiHelperFactory->getEntitySavingHelper( $this );
116
		$this->stringNormalizer = $wikibaseRepo->getStringNormalizer();
117
		$this->enabledEntityTypes = $wikibaseRepo->getLocalEntityTypes();
118
119
		$this->entitySavingHelper->setEntityIdParam( 'id' );
120
121
		$this->setServices( new SiteLinkTargetProvider(
122
			$wikibaseRepo->getSiteLookup(),
123
			$settings->getSetting( 'specialSiteLinkGroups' )
124
		) );
125
126
		// TODO: use the EntitySavingHelper to load the entity, instead of an EntityRevisionLookup.
127
		$this->revisionLookup = $wikibaseRepo->getEntityRevisionLookup( Store::LOOKUP_CACHING_DISABLED );
128
		$this->permissionChecker = $wikibaseRepo->getEntityPermissionChecker();
129
		$this->titleLookup = $wikibaseRepo->getEntityTitleLookup();
130
		$this->siteLinkGroups = $settings->getSetting( 'siteLinkGroups' );
131
		$this->badgeItems = $settings->getSetting( 'badgeItems' );
132
133
		$this->federatedPropertiesEnabled = $federatedPropertiesEnabled;
134
	}
135
136
	public function setServices( SiteLinkTargetProvider $siteLinkTargetProvider ): void {
137
		$this->siteLinkTargetProvider = $siteLinkTargetProvider;
138
	}
139
140
	protected function getTitleLookup(): EntityTitleStoreLookup {
141
		return $this->titleLookup;
142
	}
143
144
	protected function getResultBuilder(): ResultBuilder {
145
		return $this->resultBuilder;
146
	}
147
148
	/**
149
	 * Create a new Summary instance suitable for representing the action performed by this module.
150
	 *
151
	 * @param array $params
152
	 *
153
	 * @return Summary
154
	 */
155
	protected function createSummary( array $params ): Summary {
156
		$summary = new Summary( $this->getModuleName() );
157
		$summary->setUserSummary( $params['summary'] );
158
		return $summary;
159
	}
160
161
	/**
162
	 * Actually modify the entity.
163
	 *
164
	 * @param EntityDocument $entity
165
	 * @param ChangeOp $changeOp
166
	 * @param array $preparedParameters
167
	 *
168
	 * @return Summary|null a summary of the modification, or null to indicate failure.
169
	 */
170
	abstract protected function modifyEntity(
171
		EntityDocument $entity,
172
		ChangeOp $changeOp,
173
		array $preparedParameters
174
	): ?Summary;
175
176
	/**
177
	 * Applies the given ChangeOp to the given Entity.
178
	 * Any ChangeOpException is converted into an ApiUsageException with the code 'modification-failed'.
179
	 *
180
	 * @param ChangeOp $changeOp
181
	 * @param EntityDocument $entity
182
	 * @param Summary|null $summary The summary object to update with information about the change.
183
	 *
184
	 * @return ChangeOpResult
185
	 */
186
	protected function applyChangeOp( ChangeOp $changeOp, EntityDocument $entity, Summary $summary = null ): ChangeOpResult {
187
		try {
188
			// NOTE: Always validate modification against the current revision, if it exists!
189
			//       Otherwise, we may miss e.g. a combination of language/label/description
190
			//       that was already taken.
191
			// TODO: conflict resolution should be re-engineered, see T126231
192
			// TODO: use the EntitySavingHelper to load the entity, instead of an EntityRevisionLookup.
193
			// TODO: consolidate with StatementModificationHelper::applyChangeOp
194
			// FIXME: this EntityRevisionLookup is uncached, we may be loading the Entity several times!
195
			$currentEntityRevision = $this->revisionLookup->getEntityRevision(
196
				$entity->getId(),
0 ignored issues
show
It seems like $entity->getId() can be null; however, getEntityRevision() 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...
197
				0,
198
				 LookupConstants::LATEST_FROM_REPLICA_WITH_FALLBACK
199
			);
200
			if ( $currentEntityRevision ) {
201
				$currentEntityResult = $changeOp->validate( $currentEntityRevision->getEntity() );
202
				if ( !$currentEntityResult->isValid() ) {
203
					throw new ChangeOpValidationException( $currentEntityResult );
204
				}
205
			}
206
207
			// Also validate the change op against the entity it would be applied on, as apply might
208
			// explode on cases validate would have caught.
209
			// Case for that seem to be a "clear" flag of wbeditentity which results in $entity being
210
			// quite a different entity from $currentEntity, and validation results might differ significantly.
211
			$result = $changeOp->validate( $entity );
212
213
			if ( !$result->isValid() ) {
214
				throw new ChangeOpValidationException( $result );
215
			}
216
217
			$changeOpResult = $changeOp->apply( $entity, $summary );
218
219
			// Also validate change op result as it may contain further validation
220
			// that is not covered by change op validators
221
			$changeOpResultValidationResult = $changeOpResult->validate();
222
223
			if ( !$changeOpResultValidationResult->isValid() ) {
224
				throw new ChangeOpValidationException( $changeOpResultValidationResult );
225
			}
226
227
			return $changeOpResult;
228
229
		} catch ( ChangeOpException $ex ) {
230
			$this->errorReporter->dieException( $ex, 'modification-failed' );
231
		}
232
	}
233
234
	/**
235
	 * @param array $params
236
	 * @return array
237
	 */
238
	protected function prepareParameters( array $params ): array {
239
		return $params;
240
	}
241
242
	protected function validateEntitySpecificParameters(
243
		array $preparedParameters,
244
		EntityDocument $entity,
245
		int $baseRevId
246
	): void {
247
	}
248
249
	/**
250
	 * Make sure the required parameters are provided and that they are valid.
251
	 *
252
	 * @param array $params
253
	 */
254
	protected function validateParameters( array $params ): void {
255
		$entityReferenceBySiteLinkGiven = isset( $params['site'] ) && isset( $params['title'] );
256
		$entityReferenceBySiteLinkPartial = ( isset( $params['site'] ) xor isset( $params['title'] ) );
257
		$entityIdGiven = isset( $params['id'] );
258
		$shouldCreateNewWithSomeType = isset( $params['new'] );
259
260
		$noReferenceIsGiven = !$entityIdGiven && !$shouldCreateNewWithSomeType && !$entityReferenceBySiteLinkGiven;
261
		$bothReferencesAreGiven = $entityIdGiven && $entityReferenceBySiteLinkGiven;
262
263
		if ( $entityReferenceBySiteLinkPartial ) {
264
			$this->errorReporter->dieWithError(
265
				'wikibase-api-illegal-id-or-site-page-selector',
266
				'param-missing'
267
			);
268
		}
269
270
		if ( $noReferenceIsGiven || $bothReferencesAreGiven ) {
271
			$this->errorReporter->dieWithError(
272
				'wikibase-api-illegal-id-or-site-page-selector',
273
				'param-illegal'
274
			);
275
		}
276
277
		if ( $shouldCreateNewWithSomeType && ( $entityIdGiven || $entityReferenceBySiteLinkGiven ) ) {
278
			$this->errorReporter->dieWithError(
279
				'Either provide the item "id" or pairs of "site" and "title" or a "new" type for an entity',
280
				'param-illegal'
281
			);
282
		}
283
	}
284
285
	/**
286
	 * @inheritDoc
287
	 */
288
	public function execute(): void {
289
		$params = $this->extractRequestParams();
290
		$user = $this->getUser();
291
292
		$this->validateParameters( $params );
293
		$entityId = $this->entitySavingHelper->getEntityIdFromParams( $params );
294
		$this->validateAlteringEntityById( $entityId );
295
296
		// Try to find the entity or fail and create it, or die in the process
297
		$entity = $this->loadEntityFromSavingHelper( $entityId );
298
		$entityRevId = $this->entitySavingHelper->getBaseRevisionId();
299
300
		if ( $entity->getId() === null ) {
301
			throw new LogicException( 'The Entity should have an ID at this point!' );
302
		}
303
304
		$preparedParameters = $this->prepareParameters( $params );
305
		unset( $params );
306
307
		$this->validateEntitySpecificParameters( $preparedParameters, $entity, $entityRevId );
308
309
		$changeOp = $this->getChangeOp( $preparedParameters, $entity );
310
311
		$status = $this->checkPermissions( $entity, $user, $changeOp );
312
313
		if ( !$status->isOK() ) {
314
			// Was …->dieError( 'You do not have sufficient permissions', … ) before T150512.
315
			$this->errorReporter->dieStatus( $status, 'permissiondenied' );
316
		}
317
318
		$summary = $this->modifyEntity( $entity, $changeOp, $preparedParameters );
319
320
		if ( !$summary ) {
321
			//XXX: This could rather be used for "silent" failure, i.e. in cases where
322
			//     there was simply nothing to do.
323
			$this->errorReporter->dieError( 'Attempted modification of the item failed', 'failed-modify' );
0 ignored issues
show
Deprecated Code introduced by
The method Wikibase\Repo\Api\ApiErrorReporter::dieError() has been deprecated with message: Use dieWithError() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
324
		}
325
326
		try {
327
			$status = $this->entitySavingHelper->attemptSaveEntity(
328
				$entity,
329
				$summary
0 ignored issues
show
It seems like $summary defined by $this->modifyEntity($ent...p, $preparedParameters) on line 318 can be null; however, Wikibase\Repo\Api\Entity...er::attemptSaveEntity() 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...
330
			);
331
		} catch ( MWContentSerializationException $ex ) {
0 ignored issues
show
The class MWContentSerializationException does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
332
			// This happens if the $entity created via modifyEntity() above (possibly cleared
333
			// before) is not sufficiently initialized and failed serialization.
334
			$this->errorReporter->dieError( $ex->getMessage(), 'failed-save' );
0 ignored issues
show
Deprecated Code introduced by
The method Wikibase\Repo\Api\ApiErrorReporter::dieError() has been deprecated with message: Use dieWithError() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
335
		}
336
337
		$this->addToOutput( $entity, $status, $entityRevId );
338
	}
339
340
	/**
341
	 * @param array $preparedParameters
342
	 * @param EntityDocument $entity
343
	 *
344
	 * @return ChangeOp
345
	 */
346
	abstract protected function getChangeOp( array $preparedParameters, EntityDocument $entity ): ChangeOp;
347
348
	/**
349
	 * Try to find the entity or fail and create it, or die in the process.
350
	 *
351
	 * @param EntityId|null $entityId
352
	 *
353
	 * @return EntityDocument
354
	 * @throws ApiUsageException
355
	 */
356
	private function loadEntityFromSavingHelper( ?EntityId $entityId ): EntityDocument {
357
		$entity = $this->entitySavingHelper->loadEntity( $entityId, EntitySavingHelper::NO_FRESH_ID );
358
359
		if ( $entity->getId() === null ) {
360
			// Make sure the user is allowed to create an entity before attempting to assign an id
361
			$permStatus = $this->permissionChecker->getPermissionStatusForEntity(
362
				$this->getUser(),
363
				EntityPermissionChecker::ACTION_EDIT,
364
				$entity
365
			);
366
			if ( !$permStatus->isOK() ) {
367
				$this->errorReporter->dieStatus( $permStatus, 'permissiondenied' );
368
			}
369
370
			$entity = $this->entitySavingHelper->loadEntity( $entityId, EntitySavingHelper::ASSIGN_FRESH_ID );
371
		}
372
373
		return $entity;
374
	}
375
376
	/**
377
	 * Check the rights for the user accessing the module.
378
	 *
379
	 * @param EntityDocument $entity the entity to check
380
	 * @param User $user User doing the action
381
	 * @param ChangeOp $changeOp
382
	 *
383
	 * @return Status the check's result
384
	 */
385
	private function checkPermissions( EntityDocument $entity, User $user, ChangeOp $changeOp ): Status {
386
		$status = Status::newGood();
387
388
		foreach ( $changeOp->getActions() as $perm ) {
389
			$permStatus = $this->permissionChecker->getPermissionStatusForEntity( $user, $perm, $entity );
390
			$status->merge( $permStatus );
391
		}
392
393
		return $status;
394
	}
395
396
	private function addToOutput( EntityDocument $entity, Status $status, int $oldRevId ): void {
397
		$this->getResultBuilder()->addBasicEntityInformation( $entity->getId(), 'entity' );
0 ignored issues
show
It seems like $entity->getId() can be null; however, addBasicEntityInformation() 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...
398
		$this->getResultBuilder()->addRevisionIdFromStatusToResult( $status, 'entity', $oldRevId );
399
400
		$params = $this->extractRequestParams();
401
402
		if ( isset( $params['site'] ) && isset( $params['title'] ) ) {
403
			$normTitle = $this->stringNormalizer->trimToNFC( $params['title'] );
404
			if ( $normTitle !== $params['title'] ) {
405
				$this->getResultBuilder()->addNormalizedTitle( $params['title'], $normTitle, 'normalized' );
406
			}
407
		}
408
409
		$this->getResultBuilder()->markSuccess( 1 );
410
	}
411
412
	/**
413
	 * @inheritDoc
414
	 */
415
	protected function getAllowedParams(): array {
416
		return array_merge(
417
			parent::getAllowedParams(),
418
			$this->getAllowedParamsForId(),
419
			$this->getAllowedParamsForSiteLink(),
420
			$this->getAllowedParamsForEntity()
421
		);
422
	}
423
424
	/**
425
	 * Get allowed params for the identification of the entity
426
	 * Lookup through an id is common for all entities
427
	 *
428
	 * @return array[]
429
	 */
430
	private function getAllowedParamsForId(): array {
431
		return [
432
			'id' => [
433
				self::PARAM_TYPE => 'string',
434
			],
435
			'new' => [
436
				self::PARAM_TYPE => $this->enabledEntityTypes,
437
			],
438
		];
439
	}
440
441
	/**
442
	 * Get allowed params for the identification by a sitelink pair
443
	 * Lookup through the sitelink object is not used in every subclasses
444
	 *
445
	 * @return array[]
446
	 */
447
	private function getAllowedParamsForSiteLink(): array {
448
		$sites = $this->siteLinkTargetProvider->getSiteList( $this->siteLinkGroups );
449
450
		return [
451
			'site' => [
452
				self::PARAM_TYPE => $sites->getGlobalIdentifiers(),
453
			],
454
			'title' => [
455
				self::PARAM_TYPE => 'string',
456
			],
457
		];
458
	}
459
460
	/**
461
	 * Get allowed params for the entity in general
462
	 *
463
	 * @return array
464
	 */
465
	private function getAllowedParamsForEntity(): array {
466
		return [
467
			'baserevid' => [
468
				self::PARAM_TYPE => 'integer',
469
			],
470
			'summary' => [
471
				self::PARAM_TYPE => 'string',
472
			],
473
			'tags' => [
474
				self::PARAM_TYPE => 'tags',
475
				self::PARAM_ISMULTI => true,
476
			],
477
			'token' => null,
478
			'bot' => false,
479
		];
480
	}
481
482
}
483