This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
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
|
|||
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
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. ![]() |
|||
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);
}
}
![]() |
|||
330 | ); |
||
331 | } catch ( MWContentSerializationException $ex ) { |
||
0 ignored issues
–
show
|
|||
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
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. ![]() |
|||
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);
}
}
![]() |
|||
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 |
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: