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 | 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, |
||
0 ignored issues
–
show
|
|||
330 | $baseRevId, |
||
0 ignored issues
–
show
It seems like
$baseRevId defined by $this->getBaseRevisionId() on line 321 can also be of type boolean ; however, Wikibase\Lib\Store\Entit...up::getEntityRevision() does only seem to accept integer , maybe add an additional type check?
If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check: /**
* @return array|string
*/
function returnsDifferentValues($x) {
if ($x) {
return 'foo';
}
return array();
}
$x = returnsDifferentValues($y);
if (is_array($x)) {
// $x is an array.
}
If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue. ![]() |
|||
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 ); |
||
0 ignored issues
–
show
It seems like
$lastRevId defined by parameter $lastRevId on line 487 can also be of type boolean ; however, Wikibase\Lib\Store\Entit...re::userWasLastToEdit() does only seem to accept integer , maybe add an additional type check?
This check looks at variables that have been passed in as parameters and are passed out again to other methods. If the outgoing method call has stricter type requirements than the method itself, an issue is raised. An additional type check may prevent trouble. ![]() |
|||
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, |
||
0 ignored issues
–
show
It seems like
$this->user can be null ; however, getPermissionStatusForEntity() 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);
}
}
![]() |
|||
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 ) ) { |
||
0 ignored issues
–
show
It seems like
$token defined by parameter $token on line 640 can also be of type boolean ; however, Wikibase\Repo\EditEntity...EditEntity::isTokenOK() does only seem to accept string , maybe add an additional type check?
This check looks at variables that have been passed in as parameters and are passed out again to other methods. If the outgoing method call has stricter type requirements than the method itself, an issue is raised. An additional type check may prevent trouble. ![]() |
|||
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 ); |
||
0 ignored issues
–
show
It seems like
$this->user can be null ; however, run() 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);
}
}
![]() |
|||
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, |
||
0 ignored issues
–
show
It seems like
$this->user can be null ; however, saveEntity() 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);
}
}
![]() |
|||
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);
}
}
![]() |
|||
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 ); |
||
0 ignored issues
–
show
It seems like
$this->user can be null ; however, updateWatchlist() 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);
}
}
![]() It seems like
$this->getEntityId() can be null ; however, updateWatchlist() 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);
}
}
![]() |
|||
815 | } |
||
816 | |||
817 | } |
||
818 |
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: